Self-hosting (BYOC)

Use self-hosting to export a saved flow or agent as a self-contained @runtypelabs/runtime definition. Run the definition on Cloudflare Workers, Cloud Run, Node, Vercel Functions, or another environment with a runtime adapter. The runtime calls Runtype for model execution through the platform-key proxy and for hosted memory, crawl, email, search, and asset storage.

Self-hosting (BYOC, or bring your own cloud) requires an Enterprise plan. After you authenticate with the required permission, your account receives HTTP 403 with the code BYOC_PLAN_REQUIRED if it lacks an Enterprise plan. Contact support to enable self-hosting for your account.

Export a flow or agent

The export endpoints return fully resolved definitions that the @runtypelabs/runtime package consumes at startup. The flow export inlines step configurations. The agent export inlines capabilities, flows, and nested sub-agents through up to three levels, and replaces MCP credential values with secret-name references.

To export a flow, send this GET request:

Export a flow
$curl https://api.runtype.com/v1/flows/FLOW_ID/export-runtime \
> -H "Authorization: Bearer YOUR_API_KEY"

To export an agent, send this GET request:

Export an agent
$curl https://api.runtype.com/v1/agents/AGENT_ID/export-runtime \
> -H "Authorization: Bearer YOUR_API_KEY"

Replace the placeholders in both requests as follows:

  • FLOW_ID: ID of the flow to export.
  • AGENT_ID: ID of the agent to export.
  • YOUR_API_KEY: Runtype API key with the RUNTIME:EXPORT scope.

On an account without an Enterprise plan, both endpoints return this response:

1{
2 "error": "Feature Not Available",
3 "message": "Self-hosting (BYOC) requires an Enterprise plan. Contact support to enable it for your account.",
4 "code": "BYOC_PLAN_REQUIRED"
5}

Host dependencies

Some things a flow or agent can do are not self-contained: the runtime hands them to the host rather than performing them itself. An export is never refused for containing one. Instead, both export responses carry hostDependencies — the runtime seams you have to wire before the artifact can run everything it describes:

kindRuntime interfaceWhat to wire
durable-pause-hostExecuteFlowOptions.onDurablePauseDurable-class steps (wait-until, crawl) park a pause instead of blocking. The runtime hands you the pause snapshot and never persists or schedules it; store it and resume the execution later.
background-run-coordinatorRuntimeToolExecutorDeps.backgroundRunCoordinatorDetached subagents run past the parent’s turn. The coordinator owns admission, persistence, wake and re-drive, and terminal delivery; the runtime only hands it a child request and takes a handle.

ExecuteAgentOptions.onDurablePause is the agent-lane sibling of the first one, with the same contract. The durable-pause dependency is satisfied by either channel: wiring onDurablePause, or consuming the durable-paused FlowStreamOutcome yourself (the executeFlowToWriter path, where a host that already arms its own scheduler needs no hook).

The runtime does not ship a default implementation of either seam today. If you wire neither channel, a run that reaches a durable step now fails with a truthful terminal error — execution_error with the stable code DURABLE_PAUSE_HOST_NOT_CONFIGURED — instead of reporting a paused success nobody is holding. The runtime also offers a pre-flight helper you can call at boot, so a missing dependency surfaces on startup with the full list rather than mid-run at the first durable step. Defaults may ship later; an artifact that no longer needs a seam simply stops declaring it.

hostDependencies is always present. An empty array means the artifact is fully self-contained.

1{
2 "id": "flow_01k9x2q0000000000000000000",
3 "name": "Competitor watch",
4 "steps": [],
5 "hostDependencies": [
6 {
7 "kind": "durable-pause-host",
8 "interface": "ExecuteFlowOptions.onDurablePause",
9 "reason": "Durable-class steps park their pause on a host-provided durable executor. ...",
10 "steps": [
11 {
12 "stepId": "scrape_pricing",
13 "stepType": "crawl",
14 "ownerKind": "agent",
15 "ownerId": "agent_01k9x2q0000000000000000000",
16 "ownerName": "Research assistant"
17 }
18 ]
19 }
20 ]
21}

Each entry names the sites that caused it. The scan covers the artifact’s entire executable closure, not just its own top-level steps: conditional branches, embedded agents’ capabilities, and the flow or sub-agent definitions carried inline by a runtime tool. A site therefore often names a step that does not appear in the flow you exported, which is why every one carries ownerKind (flow, agent, capability, or inline-tool) with ownerId and ownerName where they exist.

Treat the list as open-ended: new kind values may appear, so render interface and reason for a kind you do not recognise rather than failing. The schema is built for that — an unrecognised kind deserializes as an entry carrying just kind, interface, and reason, with any kind-specific evidence alongside, so a client on an older SDK keeps working and can still tell you what to wire.

Two things are worth separating from this. POST /v1/runtime/crawl below is a synchronous crawl the hosted platform serves your runtime; it needs no durable pause and is unrelated to the crawl flow step. And Workers for Platforms artifacts are not self-hosted exports: Cloudflare Workflows cannot live in a WfP dispatch namespace, and that host is ours rather than yours, so a durable-class step makes an artifact permanently WfP-ineligible instead of declaring a dependency you could wire.

Managed external-agent exports

An exported external agent with skillOrchestration: 'managed' is self-contained. At execution, Runtime compiles the cached agent-card skills whose effective role is tool into ordinary A2A runtime tools, then runs the configured skillOrchestrator model through the standard agent loop. Surface/additional tools can be supplied through managedSkillOrchestration.additionalTools; no hosted API orchestration engine is required.

External credentials are never embedded as plaintext. If the authored credential was already an exact {{secret:NAME}} reference, the export preserves NAME. A hosted database credential is exported as the agent-specific reference {{secret:RUNTYPE_AGENT_EXTERNAL_AUTH_<AGENT_ID>}}. A self-hosted operator must provide that name through the runtime platform adapter’s getSecret and resolveSecretReferences methods. The hosted Runtype API recognizes the agent-specific name and resolves the sealed agent credential automatically; a BYOC adapter must map it to the corresponding secret in its own secret store.

Execution timeouts

Set execution deadlines on each executeFlow or executeAgent call:

1const response = await runtime.executeAgent(name, {
2 messages,
3 stepTimeoutMs: 30_000,
4 flowTimeoutMs: 60_000,
5 wallClockTimeoutMs: 300_000,
6})
  • stepTimeoutMs limits one step attempt. An authored fallback retry receives a fresh attempt budget.
  • flowTimeoutMs limits each virtual-flow, reflection, or final-reply turn. Each turn receives a fresh relative budget. For a delegated external agent, it limits the whole A2A call, including retries.
  • wallClockTimeoutMs is the hard budget for the whole agent invocation. Every primary, reflection, and final-reply turn receives only the execution’s remaining time, and Runtime releases the caller even when a provider or tool promise ignores cancellation. Expiry during a primary turn fails the execution with AGENT_TURN_DEADLINE_EXCEEDED; expiry between completed turns remains a normal stopReason: 'timeout' loop stop. Delegated external agents apply the same hard cap to their A2A call. When it and flowTimeoutMs both apply, the earlier finite deadline wins.

An external-agent input checkpoint persists flowTimeoutMs; the resumed peer turn receives a fresh relative budget, so time spent waiting for human input is not charged. Hosted approval and client-tool continuations likewise replay the original setting. Direct runtime callers can pass Infinity to disable only flowTimeoutMs; hosted request schemas accept finite values only.

Hosted capability endpoints

The exported runtime uses these hosted endpoints for platform capabilities. The Authenticate with a Runtype API key in the Authorization: Bearer header. The API derives tenant identity from the authenticated API key, not from request body fields.

The following table lists the hosted endpoint families and their capabilities:

EndpointCapability
POST /v1/runtime/memory/*Hosted MemoryStore for saving, recalling, and summarizing memories
POST /v1/runtime/records/*Hosted RecordStore for upserting, getting, querying, and updating records
POST /v1/runtime/crawlHosted SiteCrawler for synchronous crawls
POST /v1/runtime/email/sendHosted EmailSender for sending a rendered email
POST /v1/runtime/searchHosted WebSearcher for Exa searches
POST /v1/runtime/assetsHosted AssetStore for storing an asset

When a hosted endpoint is available, it requires an Enterprise plan and returns HTTP 403 with BYOC_PLAN_REQUIRED for an account without an Enterprise plan. Use the API Reference for the full request and response schemas.

POST /v1/runtime/crawl serves a synchronous crawl only. It is not a host for the crawl flow step, which parks a durable pause and declares a durable-pause-host dependency instead. See Host dependencies.

Use an API key with FLOWS:EXECUTE or AGENTS:EXECUTE for memory and records requests. Use an API key with FLOWS:EXECUTE for crawl, email, and search requests. Use an API key with ASSETS:WRITE for POST /v1/runtime/assets.

Reporting telemetry

POST /v1/telemetry/ingest is separate from the hosted endpoint families. It does not require an Enterprise plan. RuntimeConfig.telemetry posts to this endpoint so the deployed runtime’s executions appear in the Runtype dashboard.

Use a separate API key with the TELEMETRY:WRITE scope. Create it with the Telemetry Ingest permission group in the API-key dialog. Ingest is append-only, so give this API key no other scopes.

The endpoint accepts any valid API key, and the API logs requests from keys without the TELEMETRY:WRITE scope. Existing deployments continue to report with their valid API keys.

Use a separate API key for the hosted endpoints. A shared API key also needs the execution scopes for those endpoints, which gives it more access than telemetry requires.

If you need to report on an agent that runs entirely outside Runtype, see Reporting telemetry from an external agent.

Redacting PII from telemetry

Telemetry payloads carry step output, tool results, and agent completions, so they can contain end-user personal data. Set telemetry.piiRedaction to 'redact' to have Runtype mask detected PII (emails, phone numbers, and the rest of the detector set) before it is stored:

1const runtime = createRuntime({
2 // ...
3 telemetry: {
4 enabled: true,
5 endpoint: 'https://api.runtype.com/v1/telemetry/ingest',
6 apiKey: process.env.RUNTYPE_TELEMETRY_API_KEY,
7 piiRedaction: 'redact',
8 },
9})

An individual agent’s config.piiRedaction takes precedence over this deployment-wide setting, in both directions: an agent exported with piiRedaction: 'redact' is redacted even when the deployment sets nothing, and an agent exported with piiRedaction: 'off' is not redacted even when the deployment sets 'redact'. 'default' on an agent defers to the deployment setting.

piiRedaction on an exported agent is a host-owned policy block, so declare it handled on every executeAgent call that runs such an agent:

1const response = await runtime.executeAgent(name, {
2 messages,
3 handledAgentConfig: ['piiRedaction'],
4})

Without that declaration the runtime rejects an agent exported with piiRedaction: 'redact' before it streams, throwing UnsupportedAgentConfigError whose unsupportedPaths names config.piiRedaction. The deployment-wide telemetry.piiRedaction setting needs no declaration, because it is not agent config.

The runtime resolves the policy per execution and sends it with each event, so one deployment can run redacted and unredacted agents side by side. Omitting both settings leaves redaction off, which is the existing behavior. Secret redaction is always applied and is not affected by this setting.

The public POST /v1/assets endpoint is separate from self-hosting and is available on every plan. The runtime mount, POST /v1/runtime/assets, requires an Enterprise plan.

Firecrawl and Massive fetch methods

Standard HTTP requests in fetch-url steps run through the runtime adapter. For Firecrawl or Massive, supply a ManagedFetchExecutor through RuntimeConfig.managedFetchExecutor. The executor resolves credentials, enforces provider policy, and sends the requests.

No hosted /v1/runtime/* endpoint handles managed fetch. If you omit ManagedFetchExecutor, the default context-step executor returns a not-supported error instead of calling the provider. If you supply a custom contextStepExecutor, that executor handles the managed-fetch integration.

Embedding models beyond the built-ins

generate-embedding steps and the query embedding inside vector-search use the runtime adapter for these OpenAI and Google model IDs: text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002, text-embedding-004, and gemini-embedding-001. The adapter resolves OPENAI_API_KEY or GOOGLE_API_KEY for these models.

For any other embedding model ID, supply an EmbeddingProvider through RuntimeConfig.embeddingProvider. This includes Weaviate vectorizer IDs such as text2vec-openai, text2vec-cohere, text2vec-huggingface, and the Snowflake/snowflake-arctic-embed-* family. Your Weaviate instance produces these vectors, so the export does not include their credentials or configuration.

No hosted /v1/runtime/* endpoint handles embedding. If you omit an EmbeddingProvider, the affected steps fail with EmbeddingProviderNotConfiguredError and name the model. They do not fall back to another model. Built-in models continue to use their native provider when you configure an EmbeddingProvider.

For tests and offline runs, use StaticEmbeddingProvider. It is not a real embedder. It returns a vector derived from a hash of the input text.

Inspect each step’s embeddingModel before deployment. A flow can use a built-in model in one step and require an EmbeddingProvider in another.

Record storage and tenancy scoping

The record steps, including upsert-record, get-record, list-records, and update-record, use RuntimeConfig.recordStore. The runtime includes InMemoryRecordStore for stateless agents and tests, and HostedRecordStore for the hosted POST /v1/runtime/records/* endpoints. The default NoOpRecordStore raises an error when a record step runs.

If an exported flow or agent includes tenancyStrategy, provide both of these components:

  1. A scope-capable store: Set supportsRecordScope = true on your RecordStore implementation. Honor the scope descriptor on every call. Filter every read, including the update-record target lookup. Add the scope columns on insert. Partition upsert conflict resolution by scope so the same (type, name) can exist for two tenants.
  2. A resolved scope per execution: Resolve tenant and end-user identity in your host. Pass the result as ExecuteAgentOptions.recordScope when you call executeAgent. Pass it as ExecuteFlowOptions.recordScope when you call executeFlow for a standalone flow export.

An agent execution passes its scope to the flows that it runs. A top-level executeFlow call does not infer the scope, so pass recordScope explicitly. The export does not include Runtype identity resolution. Add tenancyStrategy to handledAgentConfig before the runtime accepts the agent.

If an execution carries a resolved scope but the store does not declare support for it, the record step fails with RecordScopeUnsupportedError. The step does not read or write the global namespace. HostedRecordStore does not declare record-scope support because the hosted API does not accept a scope descriptor, so the server cannot verify the execution’s tenant.

If you omit tenancyStrategy, the runtime uses the plain owner namespace.

Collection schema validation

If you register a record type as a collection with a metadata schema, the upsert-record and update-record steps call RuntimeConfig.collectionValidator before each write. upsert-record merges on conflict inside the store, so validation checks the incoming payload for each batch item. update-record reads the target row first, so validation checks the post-merge metadata.

The validator exposes two methods:

  • validate(type, metadata): Returns the verdict for writing metadata as a record of type. It does not throw. A lookup failure returns unevaluated.
  • prime(type): Warms the collection lookup without validating data. Call it before validation inside an open transaction. A cold lookup in that transaction can deadlock a single-session driver.

With an enforce verdict, the step rejects the write. The step’s errorHandling setting controls the failure, as it does for a payload-limit violation. With a warn verdict, the step writes and adds schemaWarnings to the step metadata. The upsert-record batch path does not report warnings.

The verdict is stored in the schemaValid write field. A store that persists that field records the same validation result as Runtype. For validation modes and violation details, see Defining a schema.

No hosted /v1/runtime/* endpoint handles collection validation. The hosted record endpoints do not evaluate collection schemas. If your self-hosted deployment needs enforcement, supply a validator backed by your collection definitions.

If you omit the validator, every verdict is unevaluated. Writes proceed without a schemaValid stamp. This behavior is intentional because validation is an owner-configured contract for metadata shape, not a security boundary. A collection set to enforce in the dashboard is not enforced by an exported flow or agent until you wire a validator.

Next steps

Continue with these guides: