Skip to navigation

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 model providers with your own provider keys, through the Runtype platform-key proxy, or both, depending on the key mode you configure. It can also call hosted Runtype endpoints for asset storage and other platform capabilities.

Before you begin

You need the following:

  • An Enterprise plan. Self-hosting (BYOC, or bring your own cloud) requires one. When an account without it calls an export endpoint with a correctly scoped API key, it receives HTTP 403 with the code BYOC_PLAN_REQUIRED. Contact support to enable self-hosting for your account.
  • An API key with the Runtime Export permission. In the Create New API Key dialog, select Runtime Export in the Permissions list. A resource wildcard such as AGENTS:* or FLOWS:* doesn’t grant this scope. Only the RUNTIME:EXPORT scope itself or the global * scope, which the Full Access permission template grants, does. For more information, see Managing API keys.

Export a flow or agent

The export endpoints return fully resolved definitions that the @runtypelabs/runtime package loads 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.

An inline MCP server with api_key or custom_header auth sends its token reference under the server’s configured header name. Tools of a server with basic auth are not listed in the export. The runtime discovers them when the agent starts and builds the Authorization: Basic header from the username and password secrets.

The export never copies a credential that an author typed into a tool configuration or an MCP server. The export returns 422 with code raw_credential_in_export in these cases:

  • A literal value sits under a credential-shaped field, such as apiKey or an X-Api-Key header.
  • A value in a known secret format, such as an OpenAI, Anthropic, or GitHub key, sits under any field.

Each entry in the response’s details array names the tool or MCP server, the field, and the JSON path. The response never includes the value. To fix the export, store the value as a Runtype secret, replace the literal with a {{secret:NAME}} reference, and provide NAME to your runtime.

You can export agents that run with a model and system prompt, and external (A2A) agents. You can’t export a Claude Managed agent: exporting one returns 404 with Agent not found. If an agent you export references a Claude Managed agent as a sub-agent, the export returns 200 without that sub-agent and lists it in warnings. A flow that references a Claude Managed agent gets a stub that the runtime can’t run, which warnings also lists. The flow export is in limited preview.

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.

The TypeScript SDK exposes the same exports as flows.exportRuntime(id) and agents.exportRuntime(id, selector). The Runtype MCP server exposes them as the export_flow_runtime and export_agent_runtime tools. The export_agent_runtime tool takes the selector as version_id or alias.

An export is a snapshot. Without a selector, the agent export follows your organization’s live-default setting. With the setting on, the export captures the version that the agent’s live alias points at, which is the version that hosted execution runs. Saved changes that you haven’t published or deployed to live aren’t included. With the setting off, the export captures the agent’s current saved configuration. Each referenced sub-agent follows the same setting. The deployed runtime makes no lookups back to Runtype for its definition, so after you change the agent, a flow, or a tool in Runtype, publish the change, export again, and redeploy.

Export a specific version or alias

To export a published version, or the version that a release alias points at, add one selector to the agent export:

Export the version behind an alias
curl "https://api.runtype.com/v1/agents/AGENT_ID/export-runtime?alias=staging" \
-H "Authorization: Bearer YOUR_API_KEY"
Export a published version
curl "https://api.runtype.com/v1/agents/AGENT_ID/export-runtime?versionId=VERSION_ID" \
-H "Authorization: Bearer YOUR_API_KEY"
  • alias: name of a release alias, such as staging, live, or pr-42. The export captures the version that the alias points at when you call it, even when live points at a different version.
  • versionId: ID of a published version of the agent.

Pass at most one selector. A request with both returns 400. The alias resolves the same way as the execution selector agent: { agentId, alias }. In the SDK, pass agents.exportRuntime(id, { alias: 'staging' }) or agents.exportRuntime(id, { versionId }).

An export that contains a published version carries a release object that records which version it contains:

{
"id": "agent_01k9x2q0000000000000000000",
"name": "Research assistant",
"release": {
"versionId": "agentver_01k9x2q0000000000000000000",
"alias": "staging",
"aliasRevision": 4
}
}
  • versionId: the version the export contains.
  • alias and aliasRevision: the alias that selected the version and the alias revision at the time of the export. Present only when an alias selected the version, including the live alias that an export without a selector follows.

An export of the agent’s current saved configuration has no release object.

Export a vendorable bundle

Add format=bundle to the agent export to download one zip archive that holds everything a self-hosted deployment needs. You don’t need access to the runtypelabs/core repository or to an npm registry for Runtype packages. The versionId and alias selectors work the same way as for the JSON export.

Export a bundle
curl -o agent-bundle.zip \
"https://api.runtype.com/v1/agents/AGENT_ID/export-runtime?format=bundle&alias=live" \
-H "Authorization: Bearer YOUR_API_KEY"

The archive contains these files:

  • agent.json: the same definition that the JSON export returns.
  • runtime/: the prebuilt @runtypelabs/runtime package for the platform’s current runtime version. It is minified ESM with its dependencies bundled in. To use it, add "@runtypelabs/runtime": "file:./runtime" to your package.json.
  • manifest.json: the runtime version and source commit, the agent versionId and alias, the entrypoints, hostDependencies, and contentHash.
  • LICENSE: the terms for the vendored runtime code. The runtime ships under the Functional Source License, Version 1.1, with an Apache 2.0 future license (FSL-1.1-ALv2).

Two exports of the same version return byte-identical archives. contentHash is the SHA-256 of the sha256sum listing of every file in the archive except manifest.json: one <sha256> <path> line per file, sorted by path. The response also sends it in the X-Runtype-Bundle-Content-Hash header. Pin this value to detect any change to the definition or the runtime.

With the CLI, run runtype agents export-runtime AGENT_ID --format bundle. In the SDK, call agents.exportRuntimeBundle(id, selector). The MCP tool export_agent_runtime with format: "bundle" reports the content hash and the runtime version, but it doesn’t return the archive bytes.

If the platform has no prebuilt runtime artifact for its current version, a bundle export returns 503 with code RUNTIME_ARTIFACT_UNAVAILABLE and reason: "artifact_missing".

The following table lists the errors that the export endpoints return:

StatusResponseCause
400Invalid agent ID format or Invalid flow TypeIDThe ID in the path is not a valid agent or flow ID.
403Insufficient permissions. Required: RUNTIME:EXPORTThe API key lacks the RUNTIME:EXPORT scope.
403Code BYOC_PLAN_REQUIREDThe account doesn’t have an Enterprise plan.
400Pass either versionId or alias, not bothThe agent export received both selectors.
404Agent not found or Flow not foundThe resource doesn’t exist, your account doesn’t own it, or it is Claude Managed.
404Code agent_not_foundThe agent that a release selector or default target resolves doesn’t exist.
404Code version_not_found, alias_not_found, alias_archived, or alias_expiredThe selected version or alias doesn’t exist on the agent, or the alias is archived or expired.
422Code selector_unsupported_agent_typeThe agent type can’t export a selected version.
422Code raw_credential_in_exportA tool configuration or MCP server holds a literal credential. details names each field.
500The skill’s resolution error messageA skill bound to the agent failed to resolve.
503Code RUNTIME_ARTIFACT_UNAVAILABLEA bundle export can’t use the prebuilt runtime artifact. The reason field says why.

A bundle export returns 503 with one of these reason values: artifact_missing, artifact_invalid, artifact_integrity, binding_missing, or source_commit_unknown.

A child that the export can’t embed doesn’t fail the export. The export returns 200 and lists the child in warnings. This includes sub-agents and flows that nest deeper than three levels.

Export warnings

Both export responses carry warnings. Each entry names a sub-agent, flow, or question set that the export left out of the artifact, or embedded only as a stub, and gives the reason. The tool or capability that references a dropped child is absent from the artifact. The array is empty when the export embedded every child. A bundle export lists the same entries in the warnings field of manifest.json.

A dropped sub-agent
{
"code": "child_dropped",
"reason": "claude_managed_unsupported",
"childKind": "agent",
"childId": "agent_01jv0000000000000000000000",
"parentId": "agent_01jv0000000000000000000001",
"toolName": "research",
"message": "Tool \"research\" was left out of the export: claude_managed agents cannot be exported to runtime yet"
}

The following table lists the fields of an entry:

FieldDescription
codechild_dropped when the artifact doesn’t contain the child. child_stubbed when the artifact contains a stub that the runtime can’t run.
reasonnot_found, claude_managed_unsupported, max_depth_exceeded (nesting deeper than three levels), invalid_flow_config (a malformed step), unresolved_tool_ref (a transform-data tool that doesn’t resolve), unsupported_mcp_auth, or question_set_unresolved (a configured Decision judgment tool whose question set doesn’t resolve).
childKindagent, flow, or question_set.
childIdThe ID of the child, when the reference names one. An inline flow has no ID.
parentIdThe agent whose capability referenced the child, when a capability referenced it.
toolNameThe capability or tool name that referenced the child.
messageA readable description.

Check warnings before you deploy an export. An empty array means that the artifact contains every sub-agent, flow, and question set that the definition references.

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

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

Host dependencies

Some things a flow or agent can do are not self-contained. The runtime hands them to your host instead of performing them itself. An export is never refused for containing one. Instead, both export responses carry hostDependencies. It lists the host hooks that you wire before the artifact can run everything it describes.

The following table lists the dependency kinds:

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 hands it a child request and takes a handle.

ExecuteAgentOptions.onDurablePause is the agent equivalent of the flow hook, with the same contract. You meet the durable-pause dependency in one of two ways:

  • Wire onDurablePause on every executeFlow or executeAgent call.
  • Consume the durable-paused FlowStreamOutcome yourself on the executeFlowToWriter path, if your host already arms its own scheduler.

The runtime has no default implementation of either hook. The following failures occur when a hook is missing:

  • If you wire neither durable-pause channel, a run that reaches a durable step fails with execution_error and the code DURABLE_PAUSE_HOST_NOT_CONFIGURED.
  • If you wire no coordinator, spawning a detached subagent throws BackgroundRunCoordinatorNotConfiguredError.

To catch a missing dependency at startup instead of mid-run, call runtime.assertHostDependencies(definition) at boot. It throws HostDependenciesNotConfiguredError (code HOST_DEPENDENCIES_NOT_CONFIGURED) with the full list. To report the gaps without throwing, for example in a health check, call runtime.unmetHostDependencies(definition), which returns the list.

The boot check counts the durable-pause dependency as met only when you set durablePauseHost: true in the createRuntime config. That setting declares that every executeFlow and executeAgent call supplies onDurablePause. A host that wires the hook but leaves the setting unset fails the check.

hostDependencies is always present. An empty array means the artifact is fully self-contained. The following response shows a flow that needs a durable-pause host:

{
"id": "flow_01k9x2q0000000000000000000",
"name": "Competitor watch",
"steps": [],
"hostDependencies": [
{
"kind": "durable-pause-host",
"interface": "ExecuteFlowOptions.onDurablePause",
"reason": "Durable-class steps park their pause on a host-provided durable executor. ...",
"steps": [
{
"stepId": "scrape_pricing",
"stepType": "crawl",
"ownerKind": "agent",
"ownerId": "agent_01k9x2q0000000000000000000",
"ownerName": "Research assistant"
}
]
}
]
}

Each entry names the sites that caused it. A durable-pause-host entry lists them in steps, each with stepId, stepType, and owner fields. A background-run-coordinator entry lists them in agents, each with owner fields, plus stepId when the site is a step’s tool block.

The scan covers the artifact’s entire executable closure, not only its own top-level steps. It includes 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. That is why each site carries ownerKind (flow, agent, capability, or inline-tool), with ownerId and ownerName where they exist.

Treat the list as open-ended. New kind values can appear, so render interface and reason for a kind that you don’t recognize instead of failing. An unrecognized kind deserializes as an entry that carries kind, interface, and reason, plus any kind-specific evidence. A client on an older SDK keeps working and can still tell you what to wire.

Bound skills

An agent export includes the skills bound to the agent in skills. Each entry holds the skill’s description, its body, and its resolved capability tools. It also records the skill it came from (skillId) and the skill version that supplied it (skillVersionId), so you can see exactly which skill versions an artifact contains.

The runtime loads an exported agent’s skills on every run. You don’t need to wire anything in your host. The export is a snapshot: a skill you edit, publish, bind, or unbind afterward doesn’t reach the artifact until you export again. If a bound skill’s tools can’t be resolved when you export, the export fails instead of leaving the skill out. Only the exported agent’s own skills are included; sub-agents run without their bound skills, as they do on Runtype.

To change the skills for one run, pass skills in ExecuteAgentOptions (or in the dispatch host’s executeOptions, which applies to every agent the host serves). A skill you pass replaces the exported skill with the same slug, and a skill with a new slug is added. Pass an empty array to run the agent with no skills.

Capability tools that a skill brings count toward hostDependencies. A skill’s sites are attributed to the skill, with ownerName set to skill:<slug>.

Managed external-agent exports

An exported external agent with skillOrchestration: 'managed' is self-contained. At execution, the runtime turns the agent-card skills that the agent uses as tools into A2A runtime tools. It then runs the configured skillOrchestrator model through the standard agent loop. To give the orchestrator more tools, pass them in managedSkillOrchestration.additionalTools.

The export never embeds external credentials as plaintext. It handles them as follows:

  • If the credential was already an exact {{secret:NAME}} reference, the export keeps NAME.
  • If Runtype stored the credential, the export writes the reference {{secret:RUNTYPE_AGENT_EXTERNAL_AUTH_AGENT_ID}}, where AGENT_ID is the agent’s ID.

Provide that secret name through your runtime adapter’s getSecret and resolveSecretReferences methods, mapped to the matching secret in your own secret store.

Deploy an agent with the CLI

The runtype deploy command exports one or more agents and writes a deployment project for Cloud Run, Cloudflare Workers, or Vercel Functions. It signs in with your stored CLI API key, which needs the RUNTIME:EXPORT scope.

To scaffold a Cloudflare Workers deployment, run this command:

runtype deploy --agent AGENT_ID --target cloudflare

The command accepts these options:

  • --agent: ID of an agent to export. Repeat the option to deploy several agents.
  • --target: cloud-run, cloudflare, or vercel. Defaults to cloud-run.
  • --output: directory for the project. Defaults to ./runtype-deploy.
  • --name: project name for package.json. Defaults to the output directory name.
  • --alias: export the version that this release alias points at for each agent, such as staging.
  • --version-id: export this published version. Requires exactly one --agent. You can’t combine it with --alias.

To build a staging image from the version behind the staging alias, run this command:

runtype deploy --agent AGENT_ID --alias staging --target cloud-run

For each agent, the command prints the version it exported and, when an alias selected it, the alias and its revision.

When it finishes, runtype deploy prints the next steps for your target: run ./setup.sh in the output directory, then the local test and deploy commands. The script builds @runtypelabs/runtime and installs dependencies. The package is not published to npm, so the script builds and packs it from the Runtype source checkout that contains the directory where you ran runtype deploy.

The generated server registers a streaming POST /agent/AGENT_NAME endpoint for each agent, where AGENT_NAME is the agent’s name as it appears in Runtype. GET /health lists the registered agent names.

runtype deploy scaffolds agents only. To deploy a flow, attach it to an agent as a flow runtime tool (toolType: 'flow' with config.flowId), then deploy that agent.

Provide secrets and provider keys

The runtime reads each secret through the adapter’s getSecret method. This covers model provider keys such as OPENAI_API_KEY, {{secret:NAME}} references, and MCP secret names in the export. The Cloudflare, Cloud Run, and Vercel adapters read each name from the deployment’s environment. A Cloudflare Worker reads it from its env bindings.

runtype deploy lists the required secrets in the generated README.md, with a command to set each one, and prints the list when it finishes. The following commands set a secret for each target:

  • Cloudflare Workers: wrangler secret put SECRET_NAME
  • Vercel Functions: vercel env add SECRET_NAME
  • Cloud Run: gcloud run deploy with --set-secrets, or an exported environment variable for local runs

Replace SECRET_NAME with a name from the list. A deployment without a required secret fails on its first model or MCP call.

Run an export in your own code

To host an export without the scaffold, load the export JSON, create a runtime, and call it. The following sample runs an exported agent on Node:

server.ts
import { readFile } from 'node:fs/promises'
import {
CloudRunAdapter,
NoOpRecordStore,
RuntimeContextStepExecutor,
RuntimeToolExecutor,
createRuntime,
exportedAgentSchema,
noopLoggerProvider,
} from '@runtypelabs/runtime'
const agent = exportedAgentSchema.parse(
JSON.parse(await readFile('./agents/research-assistant.json', 'utf-8'))
)
const adapter = new CloudRunAdapter()
const contextStepExecutor = new RuntimeContextStepExecutor({
loggerProvider: noopLoggerProvider,
recordStore: new NoOpRecordStore(),
adapter,
toolExecutor: () => toolExecutor,
})
const toolExecutor = new RuntimeToolExecutor({
adapter,
flowEngineDeps: { adapter, contextStepExecutor },
})
const runtime = createRuntime({
agents: { [agent.name]: agent },
keys: { mode: 'own' },
adapter,
contextStepExecutor,
toolExecutor,
})
runtime.assertHostDependencies(agent)
const response = await runtime.executeAgent(agent.name, {
messages: [{ role: 'user', content: 'Summarize the pricing page at example.com.' }],
})

The runtime registers each agent under its key in agents, and executeAgent resolves to a streaming Response. For a flow export, pass flows and call executeFlow.

RuntimeToolExecutor runs the agent’s HTTP, MCP, A2A, sub-agent, and flow tools. Pass it to createRuntime as toolExecutor. Without it, every tool call returns an error to the model, and the run still ends with execution_complete.

The runtime and the tool executor share one RuntimeContextStepExecutor, so the steps inside a flow tool, such as tool-call and record steps, run the same way as the agent’s own steps. Pass stores and providers, such as recordStore, memoryStore, or a sandbox for code steps, to RuntimeContextStepExecutor instead of createRuntime. To run custom code tools, also pass a sandbox to RuntimeToolExecutor.

The keys.mode setting chooses how the runtime pays for model calls:

  • own: The runtime calls each provider directly with the provider key that getSecret returns.
  • platform: The runtime sends every model call to the Runtype platform-key proxy.
  • hybrid: The runtime uses your key for the model’s direct provider when one is set, and falls back to the proxy when it’s missing. A model family with no direct provider in the runtime, such as glm-5.2, always uses the proxy in this mode, even when VERCEL_AI_GATEWAY_API_KEY is set.

The platform and hybrid modes require keys.platformKeyEndpoint, set to https://api.runtype.com/v1/runtime/proxy, and keys.platformApiKey, set to a Runtype API key with the RUNTIME:EXECUTE scope. To create one, select Self-hosted Runtime in the Permission Template list of the Create New API Key dialog. For the scope each hosted endpoint requires, see Hosted capability endpoints.

Declare host-owned agent settings

Some agent settings need your host to enforce them. The runtime rejects an agent that uses one of these settings before it streams, unless you pass the setting’s name in handledAgentConfig on each executeAgent call:

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

The following table lists each value and the agent configuration that requires it:

handledAgentConfig valueRequired when the agent sets
artifactsconfig.artifacts
loggingPolicyloggingPolicy: 'off'
memorymemory.enabled: true
piiRedactionpiiRedaction: 'redact'
sandboxsandbox.enabled: true
temporalconfig.temporal
tenancyStrategyconfig.tenancyStrategy

An agent that runs with a model and system prompt doesn’t need artifacts declared. Declare a value only after your host implements that behavior.

Declaring tenancyStrategy tells the runtime that hostContext carries the identity your host admitted. The runtime then enforces the strategy against that identity before the first model call. For details, see Record storage and tenancy scoping.

Nested calls don’t need temporal declared. An execute-agent flow step and an attached subagent tool have no previous turn, so the runtime applies config.temporal itself: groundNow adds the current time to the delegated message, and injectElapsed adds nothing. A detached subagent tool still rejects config.temporal, because your BackgroundRunCoordinator runs that child.

For an agent that runs with a model and system prompt, the runtime also rejects config.tools.perToolLimits, and no declaration changes that. Remove per-tool limits from such an agent before you export it.

If an agent fails before it streams, catch UnsupportedAgentConfigError and read its unsupportedPaths property. It names each configuration path, such as config.memory, that the runtime couldn’t accept.

Serve SDK clients that run local tools

An SDK client drives an exported agent whose tools are toolType: 'local' through POST /v1/dispatch, a paused envelope, and POST /v1/dispatch/resume. RuntimeToolExecutor raises ClientToolRequiredError for such a tool. A custom ToolExecutor receives local tools like any other tool.

The @runtypelabs/runtime/host/dispatch module provides that host as a WHATWG fetch handler. It parks each local tool call in memory, answers with the same events that the Runtype API streams, and continues the turn when the resume arrives. To use it, point an unchanged SDK client at the host by changing the client’s base URL. When you set upstream, the host forwards any request that it does not handle to the Runtype API with the caller’s API key.

The following sample creates a dispatch host that serves one set of exported agents:

import { CloudRunAdapter, createRuntime } from '@runtypelabs/runtime'
import { createDispatchHost } from '@runtypelabs/runtime/host/dispatch'
const host = createDispatchHost({
runtime: (deps) =>
createRuntime({
...deps,
agents: AGENTS,
keys: { mode: 'own' },
adapter: new CloudRunAdapter(),
}),
authorize: async (request) =>
request.headers.get('authorization') === `Bearer ${process.env.HOST_TOKEN}`,
upstream: { baseUrl: 'https://api.runtype.com' },
})
export default { fetch: host.fetch }

Replace the following:

  • AGENTS: the exported agents, keyed by agent name, as in Run an export in your own code.
  • HOST_TOKEN: an environment variable that holds the token your SDK clients send.

The host does not validate the API key that the SDK client sends, so it refuses every request with 401 until you set authorize. Pass your own check, or the literal 'allow-unauthenticated' for a host that only a trusted network can reach.

The host also accepts these options:

  • checkpointStore: A CheckpointStore that makes local-tool pauses durable, so a resume works after a process restart.
  • concurrency: How the host handles overlapping turns in one conversation: reject, supersede, or queue. The x-runtype-concurrency request header overrides it.
  • executeOptions: Timeouts and other options that the host passes to every executeAgent call, including handledAgentConfig. For the settings that handledAgentConfig declares, see Declare host-owned agent settings.
  • resolveHostContext: A function that returns the end user and tenant for one request. For details, see Give each request its own end user.

Give each request its own end user

Tools that reference {{_endUser.*}} or {{_tenant.*}}, and tools that send {{_identity.token}}, need a verified identity for each request. The host ignores the endUser, tenant, and identityProof fields in the dispatch body, because any caller that passes authorize can set them. To admit an identity, verify the caller in resolveHostContext and return the identity as a hostContext:

const host = createDispatchHost({
// ...runtime and authorize as in the previous sample
resolveHostContext: async (request, dispatch) => {
const session = await verifySession(request.headers.get('authorization'))
if (!session) return new Response('Unauthorized', { status: 401 })
return {
endUser: { id: session.userId, assurance: 'verified' },
tenant: { id: session.orgId, assurance: 'verified' },
}
},
})

Replace verifySession with your own check. It receives the request and the parsed dispatch body.

The function can return one of the following values:

  • A hostContext object: The host merges it over executeOptions.hostContext key by key. A key that you omit keeps the host-wide value.
  • undefined: The turn runs with executeOptions.hostContext only.
  • A Response: The host returns that response and does not start the turn.

If the function throws, the host returns 500 with the code HOST_CONTEXT_RESOLUTION_FAILED.

The host calls resolveHostContext once, when the turn starts. A resume runs with the context that the turn started with, including the host-wide part, even if executeOptions.hostContext has changed since. With a checkpointStore, the host saves that identity in the checkpoint, so a resume after a restart also uses it. To make {{_identity.token}} name the end user or tenant, set assurance on each identity, and pass an identity signer to your adapter as mintIdentityAssertion.

The same identities satisfy an agent’s tenancyStrategy. The runtime reads the assurance that you set on each identity when it checks the strategy’s assurance floor. If hostContext doesn’t carry the scope that the strategy requires, the turn fails with TenancyLockdownRefusedError before the first model call. For details, see Record storage and tenancy scoping.

The following sample serves an agent whose tenancyStrategy uses the end-user-isolated preset and whose tools send {{_identity.token}}. It combines the three pieces that such an agent needs: resolveHostContext admits the end user, executeOptions.handledAgentConfig declares that the host enforces tenancyStrategy, and mintIdentityAssertion signs the token.

const host = createDispatchHost({
runtime: (deps) =>
createRuntime({
...deps,
agents: AGENTS,
keys: { mode: 'own' },
adapter: new CloudRunAdapter({ mintIdentityAssertion: signIdentity }),
}),
authorize: async (request) =>
request.headers.get('authorization') === `Bearer ${process.env.HOST_TOKEN}`,
executeOptions: { handledAgentConfig: ['tenancyStrategy'] },
resolveHostContext: async (request) => {
const session = await verifySession(request.headers.get('x-session-token'))
if (!session) return new Response('Unauthorized', { status: 401 })
return { endUser: { id: session.userId, assurance: 'verified' } }
},
})

Replace signIdentity with your identity signer and verifySession with your own check. Without handledAgentConfig, every turn of the agent fails with UnsupportedAgentConfigError. If resolveHostContext admits no end user, the turn fails with the code end_user_scope_required before the first model call. A JSON response carries that code with status 403. A streamed response has already sent status 200, so it carries the code in an execution_error event.

The host does not serve POST /v1/dispatch/approve or history: 'stored' turns. It persists no transcript, so a stored-history dispatch returns STORED_HISTORY_UNSUPPORTED. Send the full conversation in messages, or keep that conversation on the Runtype API.

Execution timeouts

Set execution deadlines on each executeFlow or executeAgent call:

const response = await runtime.executeAgent(name, {
messages,
stepTimeoutMs: 30_000,
flowTimeoutMs: 60_000,
wallClockTimeoutMs: 300_000,
})
  • 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 the 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. To turn off only flowTimeoutMs, pass Infinity.

Hosted capability endpoints

The exported runtime uses these hosted endpoints for platform capabilities. 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, their capabilities, and the API key scope each one requires:

EndpointCapabilityRequired scope
POST /v1/runtime/proxyPlatform-key proxy for model calls in platform and hybrid key modesRUNTIME:EXECUTE
POST /v1/runtime/memory/*Hosted MemoryStore for saving, recalling, and summarizing memoriesRUNTIME:EXECUTE
POST /v1/runtime/records/*Hosted RecordStore for upserting, getting, querying, and updating recordsRUNTIME:EXECUTE
POST /v1/runtime/crawlHosted SiteCrawler for synchronous crawlsRUNTIME:EXECUTE
POST /v1/runtime/email/sendHosted EmailSender for sending a rendered emailRUNTIME:EXECUTE
POST /v1/runtime/searchHosted WebSearcher for Exa searchesRUNTIME:EXECUTE
POST /v1/runtime/assetsHosted AssetStore for storing an assetASSETS:WRITE
POST /v1/runtime/judgmentsHosted Decision judgments in platform and hybrid key modesRUNTIME:EXECUTE
POST /v1/runtime/extractionsHosted extract-step field extraction in platform and hybrid key modesRUNTIME:EXECUTE

The Self-hosted Runtime permission template grants RUNTIME:EXECUTE and TELEMETRY:WRITE, which covers every endpoint in the table except POST /v1/runtime/assets. RUNTIME:EXECUTE lets a key call these endpoints and nothing else. Unlike FLOWS:EXECUTE or AGENTS:EXECUTE, it can’t run your hosted Flows or Agents.

Keys created before RUNTIME:EXECUTE existed keep working. The proxy, memory, records, judgments, and extractions endpoints also accept FLOWS:EXECUTE or AGENTS:EXECUTE, and the crawl, email, and search endpoints also accept FLOWS:EXECUTE. Runtype records each call that one of these older scopes admits, so plan to move such keys to RUNTIME:EXECUTE.

Until November 9, 2026, POST /v1/runtime/proxy still accepts an API key without an execution scope, such as a key with only TELEMETRY:WRITE. It returns the x-runtype-warning response header, and the runtime logs that warning once per process. From November 9, 2026, the proxy refuses such a key with HTTP 403. Add RUNTIME:EXECUTE to the key before then.

Every /v1/runtime/* endpoint requires an Enterprise plan and returns HTTP 403 with BYOC_PLAN_REQUIRED for an account without one. For the full request and response schemas, see the API Reference.

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. For more information, see Host dependencies.

Use an API key with RUNTIME:EXECUTE for proxy, memory, records, judgments, extractions, crawl, email, and search requests. Use an API key with ASSETS:WRITE for POST /v1/runtime/assets.

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

Report telemetry

RuntimeConfig.telemetry reports the deployed runtime’s executions to Runtype as OpenTelemetry traces, posted to POST /v1/otel/v1/traces. Reporting is not part of the hosted endpoint families and does not require an Enterprise plan.

A reported run reaches the same fidelity as one Runtype ran itself: it appears in the Runs view with its trace tree, transcript, tool arguments and results, token usage, and a display-only cost estimate, and it can be captured as an eval case.

const runtime = createRuntime({
// ...
telemetry: {
enabled: true,
apiKey: process.env.RUNTYPE_API_KEY,
},
})
FieldEffect
enabledTurns reporting on.
apiKeyThe Runtype API key that authorizes the export.
baseUrlThe Runtype API origin. Defaults to https://api.runtype.com.
contentSends messages, tool arguments, and tool results. Defaults to true. Set it to false to report a run without conversation content, which also removes the transcript and eval capture.
contentMaxCharsCaps each content value, in characters. Defaults to 262,144, the most the ingest stores, and can only be lowered. The system prompt has its own budget, so a long history doesn’t truncate it.
sampleRateFraction of runs to report, decided once per run.
piiRedactionSee Redacting PII from telemetry.

The scaffolds that runtype deploy generates read the same settings from the environment, so you can change them without editing code. A Cloudflare Worker reads them from its env binding instead of process.env.

VariableEffect
RUNTYPE_API_KEYTurns reporting on.
RUNTYPE_TELEMETRY=offStops reporting even when the key is set.
RUNTYPE_TELEMETRY_CONTENT=offReports runs without messages, tool arguments, or tool results.
RUNTYPE_API_URLOverrides the Runtype API origin.

Use a separate API key with the TELEMETRY:WRITE scope. To create it, select Telemetry Ingest in the Permission Template list of the Create New API Key dialog. Ingest also accepts keys with AGENTS:WRITE, but ingest is append-only, so give this API key no other scopes.

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 use one key for both, create it from the Self-hosted Runtime template.

POST /v1/telemetry/ingest is the deprecated lifecycle endpoint. It carried pipeline rows only: no run, no trace tree, and no transcript. The runtime posts to it only when you set telemetry.endpoint explicitly, which no current deployment needs. Remove that field to report to the OpenTelemetry endpoint.

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:

const runtime = createRuntime({
// ...
telemetry: {
enabled: true,
apiKey: process.env.RUNTYPE_TELEMETRY_API_KEY,
piiRedaction: 'redact',
},
})

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.

If an exported agent sets piiRedaction: 'redact', pass handledAgentConfig: ['piiRedaction'] on each executeAgent call. Without it, the runtime rejects the agent before it streams. For more information, see Declare host-owned agent settings. 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. Secret redaction is always applied and is not affected by this setting.

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

Configure runtime providers

Some steps need a provider that you supply in the createRuntime config. No hosted /v1/runtime/* endpoint handles managed fetch, embedding models outside the built-ins, or collection validation.

Steps that reach an unconfigured provider

If you omit a provider, createRuntime uses a no-op default that raises a ProviderNotConfiguredError, such as its subclass RecordStoreNotConfiguredError. The error’s providerKind names the RuntimeConfig field to set. The following table shows how a step’s errorHandling setting handles that error:

errorHandlingResult
Not setThe step returns success: false and writes no output variable. The flow continues and counts the step as failed.
'continue'The step returns success: true, writes defaultValue to its output variable, and reports the error in error.
'fail'The step fails and stops the flow.

When errorHandling is unset, a failed step’s metadata carries providerNotConfigured with the provider kind. An explicit 'fail' stops the flow without adding this metadata field. Without errorHandling, a missing provider can’t look like a successful step.

The following table lists the steps and the provider each one needs:

StepsProvider (RuntimeConfig field)
upsert-record, update-recordrecordStore
save-memory, recall-memory, memory-summarymemoryStore
store-assetassetStore
generate-pdfdocumentRenderer and assetStore
send-emailemailSender
crawlsiteCrawler
search with a search-API providerwebSearcher
fetch-url with fetchMethod firecrawl or massivemanagedFetchExecutor
extractextractionProvider

Other steps handle a missing provider in their own way:

  • get-record and list-records return success: false for any errorHandling setting.
  • generate-embedding, store-vector, and vector-search return success: false unless you set errorHandling to 'continue'.
  • A template step without a templateRenderer returns success: false for any errorHandling setting.
  • A transform-data step without a sandbox returns success: true with defaultValue unless you set errorHandling to 'fail'.

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.

If you omit ManagedFetchExecutor, the default context-step executor returns a not-supported error instead of calling the provider. With errorHandling not set, the step returns success: false. For details, see Steps that reach an unconfigured 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.

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.

Decision judgments

The Decision judgment tool (builtin:decision_judge) runs in the runtime, as an agent tool and from a tool-call step. A configured instance judges with the question set that the export embeds in the tool’s config.questionSet. For what the export embeds, the credentials that each decision model needs, and how each key mode routes a judgment, see Run judgments in a self-hosted runtime.

To judge with your own implementation, supply a DecisionProvider through RuntimeConfig.decisionProvider. It implements judge(request). To judge a configured instance whose question set the export didn’t embed, it can also implement judgeByReference(request). For tests and offline runs, use StaticDecisionProvider. It makes no network call and answers each boolean question with 0.9, each choice question with its first option, and each score question with its highest level. To set an answer, pass a map of question name to answer to its constructor.

If you set no decisionProvider and the key mode gives the runtime no credential and no platform key for the model, the tool call fails with DecisionProviderNotConfiguredError. The message names the secret or binding that the model needs. The agent receives the error as a failed tool call, and the run continues.

If you pass a builtinToolExecutor to RuntimeToolExecutor, that executor runs the Decision judgment tool and the runtime doesn’t use a DecisionProvider.

Extract step

The extract step reads typed fields through the runtime’s ExtractionProvider. If you set no extractionProvider, the key mode picks one. With own keys, the runtime extracts with the Workers AI binding that adapter.getBinding("AI") returns. With platform keys, it calls POST /v1/runtime/extractions. With hybrid keys, it tries the binding first and then Runtype. For tests and offline runs, use StaticExtractionProvider. It makes no network call and answers each field with its override, or null.

If no provider can serve the step, it fails with ExtractionProviderNotConfiguredError, and errorHandling applies as described in Steps that reach an unconfigured provider. For the models and key modes, see Self-hosted runtimes.

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 RecordStoreNotConfiguredError when a record step runs. With errorHandling not set, the step returns success: false.

If an exported flow or agent includes tenancyStrategy, the runtime enforces it on every top-level executeAgent and executeFlow call. An agent also needs tenancyStrategy in handledAgentConfig. Without it, the runtime rejects the agent with UnsupportedAgentConfigError.

Pass the identity that your host verified for the execution as hostContext.tenant and hostContext.endUser. Before the first model call, the runtime checks that identity against the strategy’s preset:

  • internal: No identity is required. Records use the product scope.
  • tenant-isolated: Requires hostContext.tenant. Records are scoped to that tenant.
  • end-user-isolated: Requires hostContext.endUser. Records are scoped to that end user, within hostContext.tenant when you pass one.

The runtime reads each identity’s assurance level from hostContext.assurance.tenant or hostContext.assurance.endUser. If you don’t set that, it uses the assurance field on the identity object itself, such as endUser: { id: 'u_123', assurance: 'asserted' }. An identity with neither counts as verified, because your host admitted it. A strategy’s assurance floor is verified unless the strategy lowers it to asserted.

When the check fails, the call rejects with TenancyLockdownRefusedError and makes no model call. The error’s code property is one of the following values:

  • tenant_scope_required: The strategy requires a tenant, and hostContext has none.
  • tenant_assurance_below_floor: The tenant’s assurance is below the floor.
  • end_user_scope_required: The strategy requires an end user, and hostContext has none.
  • end_user_assurance_below_floor: The end user’s assurance is below the floor.

The runtime derives the record scope from the strategy and the admitted identity. It keys each scope by the identity’s projectedId, or by its id when projectedId is absent. An agent execution passes its scope to the flows that it runs. For an agent with tenancyStrategy, the runtime doesn’t read _tenant or _endUser from the variables that the caller passes. These values come only from hostContext.

If your host runs its own tenancy check and resolves a record scope itself, pass that scope as ExecuteAgentOptions.recordScope or ExecuteFlowOptions.recordScope. The runtime then skips its own check and uses your scope unchanged. A null scope makes every record step fail instead of falling back to the global namespace.

Your RecordStore must also support scopes. Set supportsRecordScope = true on your implementation, and honor the scope descriptor on every call. Filter every read, including the update-record target lookup and the record input of generate-embedding. Add the scope columns on insert. Partition upsert conflict resolution by scope, so that two tenants can each have a record with the same (type, name).

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. If the collection lookup fails, it returns { status: 'lookup_failed', type }. Do not return unevaluated for a failed lookup, because the write would then skip validation.
  • 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. If the lookup fails during prime, keep returning lookup_failed for that type so the transaction does not query again.

With an enforce or lookup_failed verdict, the step rejects the write. A lookup_failed rejection contains COLLECTION_LOOKUP_FAILED. 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.

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: