Flow validation warnings

Runtype validates a Flow when you save it. It also validates a Flow when you call the validate endpoint (POST /v1/public/flows/validate) or the validate_flow MCP tool. The API validates a Flow when you create or update it. These checks catch configuration mistakes that can pass schema validation and fail at runtime. Runtype reports these issues as non-blocking warnings, so you can fix them before dispatch.

How validation warnings work

Validation runs when you save a Flow in the dashboard. It also runs when you call the validate endpoint (POST /v1/public/flows/validate) or the validate_flow MCP tool. The API validates a Flow when you create or update it. The response contains one validation object:

1{
2 "valid": true,
3 "errors": [],
4 "warnings": [
5 {
6 "code": "UPSERT_RECORD_SOURCE_NOT_JSON",
7 "message": "Source variable \"analysis_result\" is set by prompt step ...",
8 "path": "flowSteps[3].config.sourceVariable",
9 "step": { "index": 3, "name": "Save analysis", "type": "upsert-record" }
10 }
11 ],
12 "recommendations": [],
13 "context": {
14 "authenticated": true,
15 "accountChecksPerformed": true,
16 "accountChecksSkipped": false
17 }
18}

When errors is empty, valid remains true and warnings do not block a save. Read warnings[].code to identify the problem and warnings[].message for the suggested fix. The path and step fields identify the step that triggered the warning.

This page lists each validation code, its trigger, and its fix. Most issues appear in warnings[]. Optimization hints that do not predict a failure appear in recommendations[] instead.

List Records output uses object access

Code: LIST_RECORDS_OBJECT_ACCESS

A list-records step returns an array of records, even when one record matches. If a later step reads a field with object-style access ({{customers.title}}) instead of indexing the array first ({{customers.0.title}}), the reference resolves to undefined at runtime.

The validation message names the variable and field, and gives the fix:

Variable "{{customers.title}}" reads field "title" on "customers", but step "Find customers" returns an ARRAY of records (even when a single record matches). Object-style access resolves to undefined at runtime. Index into the array first — e.g. "{{customers.0.title}}" for the first record — or loop over "customers". To get a single record object, use a get-record step.

A get-record step returns one object, so object-style access works and this warning does not fire. A deprecated retrieve-record step is checked according to the step type that its configuration resolves to. For more information, see Migrating from retrieve-record and Using record steps (get/list/upsert).

Deprecated retrieve-record step type

Code: DEPRECATED_STEP_TYPE

The retrieve-record step is deprecated. Replace it with Get Record and List Records. Runtype resolves an ID-based lookup as get-record and a type, name, or filter lookup as list-records at execution time.

This warning reminds you to migrate the step when you next edit the Flow. The warning does not block a save. The step continues to run with its resolved type.

The warning message names the replacement step:

The "retrieve-record" step is deprecated. Use "get-record" (returns a single record object) or "list-records" (returns an array of records) instead — this config resolves to "get-record".

Upsert-record source variable is not a JSON object

Code: UPSERT_RECORD_SOURCE_NOT_JSON

An upsert-record step’s sourceVariable must reference a JSON object because the record metadata field accepts a JSON object. A common mistake is to send plain-text output from a prompt step, with a responseFormat other than json, directly to upsert-record without a contentField. That output is a string, so the step fails at runtime.

The validation message names the prompt step and gives three fixes:

Source variable "analysis_result" is set by prompt step "Analyze" (responseFormat "text"), which outputs a string, but the record metadata column requires a JSON object. This will fail at runtime. Either: (1) set that prompt's responseFormat to "json", (2) add a transform-data step to wrap the string, or (3) set contentField on this upsert-record step to auto-wrap the string as { [contentField]: content }.

Transform-data language does not match the sandbox provider

Code: TRANSFORM_LANGUAGE_PROVIDER_MISMATCH

A transform-data step triggers this warning when its language is not javascript, including python and typescript. It also triggers when sandboxProvider is a JavaScript-only environment: Cloudflare Worker or QuickJS. The warning applies when sandboxProvider is unset because the default is Cloudflare Worker.

The pair has no compatible executor. The step can treat the source as JavaScript or fail to produce output. Because transform-data continues on error by default, you might see an empty result instead of a visible failure.

The validation message names the incompatible settings and the fix:

Step requests language "python" but sandboxProvider "cloudflare-worker" only runs JavaScript. Switch to the "runtype-sandbox" or "daytona" provider to run this language, or set language to "javascript".

To fix the mismatch, switch the execution environment to Runtype Sandbox or Daytona Sandbox, or set language to javascript. Both Runtype Sandbox and Daytona Sandbox run JavaScript, TypeScript, and Python. In the flow editor, selecting a JavaScript-only environment clears the language field.

This warning does not block a save. Flows saved before this check can still contain the mismatched pair.

Store-vector source is unresolved

Code: STORE_VECTOR_SOURCE_UNRESOLVED

A store-vector step resolves vectorsSource by looking up a variable. The variable must come from an earlier step or a Flow input. If neither source defines it, dispatch fails with a field-specific error explaining that vectorsSource did not resolve to an embedding.

The validation message names the missing variable and a typical source:

Vectors source "{{embeddings}}" references variable "embeddings", but no earlier step declares an output variable "embeddings" (and it is not a flow input). This will fail at runtime because vectorsSource does not resolve to an embedding. Set a prior step's outputVariable to "embeddings" (typically a generate-embedding step), or declare "embeddings" as a flow input.

To fix the warning, set an earlier step’s outputVariable to embeddings, or declare embeddings as a Flow input. A generate-embedding step commonly produces this value.

Store-vector source does not carry the embedding model

Codes: STORE_VECTOR_MODEL_MISSING (appears in errors[]) and STORE_VECTOR_SOURCE_MODELLESS (appears in recommendations[])

A store-vector step has no embeddingModel setting. The output envelope from a generate-embedding step carries the model and dimensions with the vector. The pgvector destination requires that model for two reasons: the embedding_model column is part of the vector-search step’s predicate, so a model-filtered vector search could never return a stored row without one, and the label must describe the vector stored beside it. (The built-in semantic search record tool does not filter by model, so it can still surface such rows, but the label requirement stands for data integrity.) A store whose source resolves without a model fails at execution time instead of storing the row.

Two single-mode source shapes drop the envelope, and validation reports them at different severities:

  • A .embedding path into a generate-embedding output, such as emb.embedding. This path returns the bare vector without the model or dimensions, so the step is guaranteed to fail at execution time. Validation reports STORE_VECTOR_MODEL_MISSING as a blocking error. (Other paths, such as emb.model, do not return a vector and fail the step at runtime for a different reason.)
  • A transform-data output. Your script defines this output’s shape, and it might preserve the whole envelope, so validation reports STORE_VECTOR_SOURCE_MODELLESS as a non-blocking recommendation. Fabricating unrelated model and dimensions values is worse than omitting them: the pair passes the model check and overwrites a correct stored model.

For a dot-path source, the error message has this form:

Vectors source "emb.embedding" reads "embedding" off "Embed" (generate-embedding), which extracts only the vector and drops the envelope's model and dimensions. store-vector has no embeddingModel config field, and the pgvector destination requires the embedding model (the embedding_model column is part of the vector-search step's model-filtered predicate, and the label must describe the stored vector), so this step fails at execution time instead of storing a row model-filtered vector search could never find. Reference the whole output variable ("emb") so the model and dimensions travel with the vector.

When the step explicitly sets errorHandling to continue, the message says the store “has its store refused and swallowed at execution time” instead of “fails at execution time”, because that configuration substitutes the step’s default value rather than failing.

For a transform-data source, the recommendation message has this form:

Vectors source "enriched" comes from "Enrich" (transform-data), whose output shape is script-defined. store-vector has no embeddingModel config field: unless the script preserves the generate-embedding envelope's "model" string and numeric "dimensions", the pgvector destination refuses the store at execution time (a modelless row would be invisible to the vector-search step's model-filtered predicate). Fabricating BOTH keys with unrelated values (e.g. the LLM model that produced the text) is worse — it passes the model-carrier check and overwrites a previously-correct embedding_model. Spread the whole generate-embedding envelope when enriching (e.g. { ...embeddingOutput, ...extras }), or feed store-vector from the generate-embedding output directly.

Batch stores get their own coverage. Each batch item must carry its own non-empty embedding_model string, and a batch-mode generate-embedding step stamps it automatically. When a pgvector batch store reads its items from a transform-data output, validation reports STORE_VECTOR_SOURCE_MODELLESS as a recommendation naming the required per-item key.

To fix any of these, set vectorsSource to the whole generate-embedding output, such as emb, not emb.embedding. If a transform-data step enriches the embedding, spread the whole envelope in the script so model and dimensions remain available. For a vector produced outside generate-embedding, supply an object with an embedding array plus a model string and numeric dimensions (single mode), or stamp embedding_model on each item (batch mode), so the model label travels with the vector.

At execution time, the step’s errorHandling setting decides what a modelless store does. With no errorHandling set, the step reports a failure and writes nothing. An explicit continue substitutes the step’s defaultValue when one is set, and otherwise the step’s standard error envelope (destination, storedCount: 0, error). An explicit fail stops the flow. A batch store applies the rule per item: an item without an embedding_model value is recorded as a per-item No embedding model failure while valid items still store.

These checks apply only to stores that target pgvector. Weaviate and Vectorize keep the vector in their own indexes, so a bare vector works for them. A root reference to a generate-embedding output is valid and does not trigger either code. If no earlier step produces the root variable, Runtype reports STORE_VECTOR_SOURCE_UNRESOLVED instead.

Tool call strategy required on a multi-step prompt

Code: TOOL_STRATEGY_REQUIRED_MULTISTEP

Setting toolCallStrategy to required on a prompt step forces a tool call on every step. Unless the turn is limited to one tool call, the model cannot return a final text answer, so the output is empty. This warning fires when maxToolCalls is unset, set to a value greater than 1, or the prompt runs in a multi-turn loop.

The validation message describes the supported configurations:

Tool call strategy "required" forces the model to call a tool on every step. Unless the turn is capped to a single tool call (maxToolCalls of 1), the model can never return a final text answer and the output comes back empty — the runtime default allows multiple tool calls, so leaving maxToolCalls unset (or above 1), or running a multi-turn loop, triggers this. Use "auto" (recommended) so the model decides when to call tools and can finish with text, or keep "required" only for a single forced tool call (maxToolCalls of 1, no loop).

Set toolCallStrategy to auto so the model can finish with text. Keep required only when you limit the turn to one forced tool call with maxToolCalls set to 1 and no loop. For more information, see Using prompt steps.

Tool call strategy none with tools attached

Code: TOOL_STRATEGY_NONE_WITH_TOOLS

Setting toolCallStrategy to none forbids the model from calling a tool. When the same prompt step also attaches tools through toolIds, runtimeTools, mcpServers, a subagent pool, or a code-mode pool, none of those tools can ever run. They are dropped from the request, and the step answers as if no tools existed.

The validation message names the attached sources:

Tool call strategy "none" disables tool calling, but tools are attached (toolIds), so none of them can ever be called: the tools are dropped from the request and the answer comes back as if no tools existed. Use "auto" (recommended) so the model decides when to call a tool, or remove the attached tools if the step should answer from the prompt alone.

Set toolCallStrategy to auto so the model can call the tools, or remove the attached tools when the step should answer from the prompt alone. For more information, see Using prompt steps.

Provider-native tools from more than one provider

Code: PROVIDER_TOOLS_MIXED_OWNERS

Some catalog tools are executed by the model’s own provider rather than by Runtype: builtin:anthropic_web_search and builtin:anthropic_web_fetch (Anthropic), builtin:openai_web_search (OpenAI), and builtin:xai_web_search and builtin:xai_x_search (xAI). A prompt step that attaches such tools is routed to the provider that owns them. One request reaches one provider, so when a step attaches provider-native tools owned by two providers, only one owner’s tools can run. The step is routed to the owner of the first provider-native tool in toolIds, and the other owner’s tools are dropped or sent as inert function tools with no error.

This warning fires when the provider-native tools on a single top-level prompt step resolve to more than one owning provider. Managed tools such as builtin:exa never count, and neither do ids the catalog does not know. details.winner names the owner the step is routed to, details.owners lists every owner, and details.toolIdsByOwner groups the attached ids by owner. The same check runs on an agent’s tool ids.

The validation message names the winner and the tools that go inert:

Provider-native tools from more than one provider are attached (anthropic: builtin:anthropic_web_search; openai: builtin:openai_web_search), but one request reaches one provider, so only the anthropic tools will run: the request is routed to anthropic because its tool is listed first, and builtin:openai_web_search will be sent as an inert function tool or dropped without a signal. Keep the tools of a single provider on this step, or move the other provider's tools to a separate prompt step whose model that provider serves.

Keep the provider-native tools of one provider on the step, or move the other provider’s tools to a separate prompt step whose model that provider serves. For more information, see Using prompt steps.

Optional parameter interpolated into an external tool template

Code: OPTIONAL_PARAM_IN_TOOL_TEMPLATE

An external runtime tool interpolates {{parameter}} into its URL, a header value, or its request body. Runtype substitutes those templates from the arguments the model supplies. An argument the model leaves out is preserved in the request as its literal {{parameter}} text, so the outbound call is structurally broken and the third-party service usually answers with a confusing 4xx.

This warning fires when the templated name is declared in parametersSchema.properties, is absent from parametersSchema.required, and the template has no fallback. One warning is reported per tool and parameter, and details.locations lists where the parameter appears (url, headers, body).

The validation message names the tool and the parameter:

External tool "weather" interpolates the optional parameter "units" into its url. An argument the model omits is left in the request as the literal text {{units}}, so the call goes out structurally broken (usually a confusing 4xx from the third party). Add "units" to parametersSchema.required, or give the template a fallback so a missing value renders as something, e.g. {{units ?? ''}}.

Add the parameter to parametersSchema.required when the request cannot work without it. When it really is optional, give the template a fallback, such as {{units ?? 'metric'}}, so a missing value renders as a real value. Parameters that are not declared in parametersSchema.properties, {{secret:NAME}} references, and templates that already carry a fallback are not reported.

Conditional compares an unquoted template against a string

Code: CONDITION_UNQUOTED_TEMPLATE_COMPARISON

A condition or when predicate uses JavaScript after template substitution. If you compare an unquoted {{...}} placeholder with a string literal, such as {{analysis.health}} === 'watch', a non-numeric value becomes a bare identifier. The expression (healthy === 'watch') then throws a ReferenceError at runtime.

The validation message shows the expression and the fix:

Expression compares an unquoted {{...}} placeholder against a string literal (`{{analysis.health}} === 'watch'`). `condition` / `when` predicates are JavaScript evaluated after template substitution, so a non-numeric value substitutes in as a bare identifier (e.g. `healthy === 'watch'`) and throws "ReferenceError: <value> is not defined" at runtime. Quote the placeholder — e.g. '{{var}}' === '...' — or compare numerically.

For a string comparison, quote the placeholder: '{{analysis.health}}' === 'watch'. For a numeric value, compare the value as a number. For more information, see Using conditional steps.

System prompt embeds a per-run variable

Code: CACHE_VOLATILE_SYSTEM_PROMPT (appears in recommendations[])

A prompt step’s system prompt embeds a per-run temporal variable such as {{_now}}, {{_execution.*}}, or {{_schedule.*}}. The system prompt changes on every execution, so automatic prompt caching in Runtype cannot reuse the cached prefix across requests. This recommendation identifies a non-blocking optimization issue. The Flow runs the same way, but the cache has fewer reusable prefixes.

The recommendation message names the variable and the fix:

System prompt for step "Assistant" embeds per-run variable(s) {{_now}}, which change on every execution. This prevents the platform's automatic prompt caching from reusing the system prefix across requests (a latency and cost optimization for multi-turn agents and batch runs). To cache better, move the changing value into the user prompt or a later step so the system prompt stays identical across runs. This does not affect correctness.

Move the changing value into the user prompt or a later step so the system prompt stays identical across runs. For more information, see Prompt caching.

Template references a reserved identity field

Code: RESERVED_IDENTITY_VARIABLE (appears in recommendations[])

A template references {{_tenant.projectedId}} or {{_endUser.projectedId}}. Runtype creates a durable projection ID (tnt_* or eu_*) for a resolved tenant or end user and uses it to scope Records, Secrets, and approval grants. Runtype keeps this ID out of the identity that prompts receive, so the reference resolves to an empty value instead of the ID. This recommendation does not block a save or run.

projectedId is a reserved key on both _tenant and _endUser. If you supply this key in the dispatch request, Runtype drops it instead of passing it through. Other fields remain addressable.

The recommendation message names the reference and the replacement:

Variable "{{_tenant.projectedId}}" is host-side plumbing, not part of the _tenant template contract. The durable projection id scopes records, secrets, and approval grants server-side and is deliberately kept out of prompts, so this reference resolves to an empty value. Use "{{_tenant.id}}" instead.

Use {{_tenant.id}} or {{_endUser.id}}, the identity that you asserted on the request, when a template needs to address the tenant or end user. For more information, see Flow variables and templates.

Memory is shared across every version and alias

Code: MEMORY_SHARED_ACROSS_POINTERS (appears in recommendations[], so it never blocks anything)

An Agent enables memory but nothing keys its bucket on the release pointer, so every version, every release alias, and every request pinned to a versionId read and write the same memory. This is the default and it is deliberate: memory is about the subject, not about which configuration version answered. The Agent runs exactly as written.

Act on it once a preview alias starts writing memory that live traffic should not see. Append the pointer to memory.profileTemplate:

  • {{_endUser.id}}/{{_agent.alias || "live"}} gives each preview alias its own bucket and keeps live on the shared one. The || "live" fallback matters: {{_agent.alias}} is absent on a request with no selector and on a request pinned to a bare versionId, and an unresolved variable disables memory for that execution.
  • {{_endUser.id}}/{{_agent.versionId}} starts every version empty, for setups where a version’s behavior must be reproducible.

When the Agent sets a tenancyStrategy, Runtype computes the namespace and ignores profileTemplate, so set memory.pointerScope to "alias" or "version" instead. The recommendation stops once the mechanism that actually applies names the pointer: the template on an ordinary Agent, pointerScope on an Agent with a Tenancy Strategy. Setting pointerScope on an Agent without one changes nothing and reports MEMORY_POINTER_SCOPE_IGNORED instead. For the full ladder, including what happens on rollback and on retargeting an alias, see Memory namespaces and release pointers.

Pointer-keyed memory template has no fallback

Code: MEMORY_POINTER_TEMPLATE_UNGUARDED (appears in recommendations[])

An Agent’s memory.profileTemplate names {{_agent.alias}} or {{_agent.versionId}} with no fallback. Those variables resolve only when the request selects a version, and Runtype disables memory for an execution whose template does not resolve. So the Agent that meant “isolate previews” silently runs with memory OFF on every selector-free request, which today is all ordinary traffic.

Give the reference a fallback so selector-free runs land on a real bucket:

{{_endUser.id}}/{{_agent.alias || "live"}}
{{_endUser.id}}/{{_agent.versionId || "live"}}

The same code fires with details.defect: "label-only" when the template keys the bucket on {{_agent.version}} and nothing else. That variable is the version’s human label: free text, not unique across versions, and absent on an unlabelled version. Key on {{_agent.versionId}} and keep the label for display.

Memory pointer scope has no effect

Code: MEMORY_POINTER_SCOPE_IGNORED (appears in warnings[])

An Agent sets memory.pointerScope to "alias" or "version", but has no tenancyStrategy. Runtype composes a pointer segment into the memory namespace only on the locked-down tenancy path. Every other Agent resolves memory.profileTemplate, which never reads pointerScope, so the Agent that asked for per-pointer isolation quietly keeps one shared bucket.

Fix it in whichever direction you meant:

  • To isolate by pointer without a Tenancy Strategy, key the template instead: {{_endUser.id}}/{{_agent.alias || "live"}} for preview isolation, or {{_endUser.id}}/{{_agent.versionId}} for per-version buckets.
  • To have Runtype compute the namespace, set a tenancyStrategy on the Agent. pointerScope then takes effect.
  • If sharing was intended, remove pointerScope.

For the full ladder, see Memory namespaces and release pointers.

Runtime tool claims a reserved tool name

Code: RESERVED_TOOL_NAME (appears in errors[], so it blocks the save)

A runtime tool’s name collides with runtype_set_state, the name Runtype reserves for the Agent state write tool. An Agent with state is told to call that name, so a tool answering to it would receive whatever the Agent writes to state.

Tool names are lowercased and every non-alphanumeric character becomes _ before the model sees them, so runtype-set-state, RUNTYPE_SET_STATE, and runtype.set.state are reserved alongside the exact spelling. runtype_ is a reserved tool namespace in general.

The error message names the tool and the reserved name:

Runtime tool "runtype-set-state" collides with the reserved tool name "runtype_set_state" (the agent state write tool). Tool names are lowercased and non-alphanumeric characters become "_" before the model sees them, so every spelling that normalizes to it is reserved with it. "runtype_" is a reserved tool namespace. Rename the tool.

Rename the tool to anything outside the runtype_ namespace. This check runs offline too, so the SDK and the dashboard report it before you save. For more information, see Agent tools.

Tool-call step points at an unknown catalog tool

Code: TOOL_CALL_STEP_UNKNOWN_CATALOG_TOOL (appears in errors[], so it blocks the save)

A tool-call step’s config.toolId starts with builtin: or platform:, but no catalog tool answers to that ID. A known catalog ID is valid: a tool-call step invokes built-in and platform tools directly, alongside a saved tool’s tool_... ID or its tool:<name> reference.

The error message names the tool and how to fix the reference:

Tool "builtin:browzer:open" is not a built-in or platform catalog tool. Check the id against the tool catalog (list_tools), or point toolId at a saved tool's "tool_..." id or its "tool:<name>" reference.

Fix the ID against the tool catalog (list_tools in MCP, or GET /v1/tools). For more information, see Built-in tools.

A tool-call step is deterministic: it runs at a fixed point in the Flow with the parameters you wrote, and no model chooses it. It therefore has no approval gate, for a catalog tool exactly as for a saved tool. A step on builtin:send_email or a builtin:browser:* tool runs unattended every time the Flow reaches it. Approval gating applies to model-chosen tool calls in an Agent or a prompt step.

Execute-agent step uses a template variable for the Agent

Code: EXECUTE_AGENT_TEMPLATED_REF_UNRUNNABLE

An execute-agent step’s config.agentId is a template expression such as {{agentVar}} instead of a fixed Agent ID. The runtime lane is the only Flow engine, and it cannot resolve an Agent it only learns about at run time, so it folds the reference as ineligible:unresolvable-agent-ref and refuses the whole Flow with 422 RUNTIME_LANE_INELIGIBLE on every surface, before any step runs.

The warning does not block the save, so an existing Flow keeps its definition. The Flow does not run until you replace the reference.

The warning message names the reference and the fix:

The agent reference "{{agentVar}}" is a template expression, so no engine can run this step: the runtime lane is the only flow engine and it refuses the whole flow with RUNTIME_LANE_INELIGIBLE (ineligible:unresolvable-agent-ref) on every surface, before any step runs. A templated reference cannot be checked against the approval, client-tool and sandbox gates a fixed reference is checked against. Point agentId at a saved agent, or put a conditional step ahead of it that chooses between fixed agents.

To pick between Agents at run time, put a conditional step ahead of the call and give each branch its own execute-agent step with a fixed Agent ID. Runtype checks each of those references statically, so the approval, client-tool, and sandbox gates still apply.

The Flow-level RUNTIME_LANE_INELIGIBLE recommendation does not report this case. That recommendation drops every reason that depends on resolving Agents, because save-time validation has no Agent registry. This step-level warning is what reports a templated reference.

Record step writes an undeclared collection field

Code: RECORD_FIELD_NOT_IN_SCHEMA

When an upsert-record or update-record step targets a schematized collection, Runtype checks the fields that the step writes against the collection schema. If the schema does not declare a field and sets additionalProperties to false, Runtype flags the write in warn mode. The runtime rejects it in enforce mode. Runtype reports this warning at save time.

Runtype checks only fields that it can read statically: the keys in an update-record step’s updates map and the contentField of an upsert-record step. The warning is independent of the collection’s validationMode because the schema defines the collection’s data model in either mode. An open additionalProperties setting accepts extra fields, so the warning does not fire.

The validation message names the field, collection, and schema version:

Field "nickname" written by this update-record step is not declared in the "customers" collection schema (v3), which does not allow additional properties. This write is flagged in "warn" mode and rejected in "enforce" mode at runtime. Add "nickname" to the collection schema or remove it from the step.

Add the field to the collection schema. For more information, see Defining a schema. You can also remove the field from the step’s updates or contentField.

Record step leaves a required schema field unmapped

Code: RECORD_REQUIRED_FIELD_UNMAPPED (appears in recommendations[])

When an update-record step uses mergeStrategy: "replace", the step replaces the record’s entire metadata value. Runtype therefore checks whether the step sets every field that the target collection schema marks required. If the step does not map a required field, Runtype reports this recommendation before the replaced record fails schema validation.

This recommendation applies only to complete replace mappings and is independent of the collection’s validationMode. A merge or deep-merge write can inherit a required field from the existing record, so it does not trigger this recommendation.

The recommendation message names the required field, collection, and schema version:

Required field "name" of the "customers" collection schema (v3) is not set by this update-record step, which replaces the record metadata. The written record will be missing a required field.

Add the required field to updates, or remove it from the schema’s required list. For more information, see Defining a schema.

Flow exceeds the runtime lane

Code: RUNTIME_LANE_INELIGIBLE (appears in recommendations[])

The runtime lane is the only Flow engine. Every run (interactive dispatch, batch, eval, and schedule) refuses a Flow the runtime lane cannot host: interactive dispatch answers 422 with code RUNTIME_LANE_INELIGIBLE and the same ineligible:* reasons before any step runs. This recommendation names the capability the runtime lane cannot host. It does not block a save.

A Flow exceeds the runtime lane when it includes any of the following capabilities:

  • A top-level wait-until or crawl step when dispatch does not provide durable hosting. Interactive dispatch with durable hosting can run a top-level durable pause on the runtime lane. A wait-until or crawl step nested inside a top-level conditional can also run on the runtime lane. It exceeds the runtime lane only when resuming the pause would require a flow definition the resume cannot re-execute.

    Dispatch-level policy no longer prevents the pause from arming. A human approval gate, per-dispatch client tools, tool-search state, a memory profile, an approval-gated step, or a browser-executed tool ahead of the pause all let the Flow arm its durable pause as usual. The detached resume then fails with actionable guidance instead of falling back to the legacy engine, because no engine can answer an approval or a browser tool without an attached client. To complete such a run, re-dispatch it attached, or remove the blocking configuration.

  • A custom tool configured for a container-backed sandbox, a non-JavaScript language, or the legacy code-mode surface. The runtime lane’s sandbox executor runs only JavaScript for this tool type.

  • A transform-data step with no executor for its language and sandbox-provider pairing. For example, the pairing can use Python or TypeScript with the Cloudflare Worker or QuickJS provider. The language and provider mismatch warning identifies the same pairing at save time.

  • A vector step that the runtime lane cannot represent, such as a store-vector step with no vector id source or a Weaviate routing. A non-default Vectorize index selected with vectorizeConfigId runs on the runtime lane.

  • An unrecognized step type.

The wait-until and crawl steps depend on the dispatch surface. Save-time validation cannot identify the dispatch surface or its durable-hosting support, so this recommendation names the capability without selecting one engine for every dispatch.

Runtype reports this recommendation at the Flow level, so its path is flowSteps on the Flow validation surface. Product and full product object (FPO) validation also reports it for an inline capability Flow that exceeds the runtime lane. In that response, path is capabilities[N].flow. The check applies only to inline Flows. Runtype does not re-check a capability that references an existing Flow or Agent by ID. That Flow or Agent receives the recommendation when you save it.

A human approval gate does not by itself cause this recommendation. The runtime lane applies a prompt step’s tools.approval policy, pauses with approval_start, and resumes through POST /v1/dispatch/approve. Approval-gated Flows therefore run on the runtime lane when you dispatch them from the API, the authenticated dashboard Run interface, or Agent execution. Batch, eval, schedule, webhook, messaging, MCP, and A2A surfaces cannot host an approval prompt, so they refuse the run with ineligible:surface-approval-pause. Client Chat instead fails closed before execution. The dispatch surface determines this behavior, so RUNTIME_LANE_INELIGIBLE does not report it. The approval enforcement warning covers it instead.

The message names the unsupported capabilities, and details.reasons contains the stable ineligible:* codes:

This flow exceeds the runtime lane, so every run (dispatch, batch, eval and schedule) refuses it; the legacy flow engine is retired. The runtime lane does not support: a wait-until (durable pause) step (ineligible:wait-until).

To run the Flow everywhere, remove or rewrite the flagged capability. For example, configure a custom tool with the Cloudflare Worker or QuickJS sandbox and JavaScript. If you leave the Flow unchanged, every run refuses it. The legacy flow engine that used to run such a Flow from interactive dispatch is retired (ADR 0022).

A transform-data step that runs Python or TypeScript on a container-backed provider does not by itself cause the Flow to exceed the runtime lane. Those pairings run on the runtime lane because dispatch selects a matching sandbox executor for each step. A pairing with no executor, such as Python with the Cloudflare Worker provider, still exceeds the runtime lane.

For authenticated validation, this check also resolves the Agents that an execute-agent step references and includes their runtime-lane capabilities. The recommendation therefore fires when a referenced Agent carries an ineligible capability, such as a custom tool that requires a container-backed sandbox. An execute-agent reference that does not resolve, such as a deleted Agent or a template {{...}} reference, is not reported as ineligible here. Runtype does not double-count it against the account-scoped AGENT_NOT_FOUND check. Unauthenticated validation cannot resolve Agents, so it reads only the Flow’s steps and configuration.

Agent cannot run on the runtime lane

Code: RUNTIME_AGENT_LANE_INELIGIBLE (appears in the save response’s _warnings)

The runtime lane is the only Agent engine. Dispatch runs an Agent there when it can host every capability in the definition; when it cannot, the dispatch is refused. When you save an Agent whose definition the runtime lane cannot host, the save response includes this non-blocking advisory in _warnings. The advisory does not block the save, but the Agent will not run until you address it. Claude Managed Agents and external Agents in Delegate mode use their dedicated execution paths, so this advisory does not apply to them. External Agents with skillOrchestration: "managed" require the Runtime quick-agent lane; if a host cannot admit that managed definition, execution fails closed instead of falling back to direct external delegation.

Before the legacy Agent engine was retired, this advisory described an engine choice: an Agent the runtime lane could not host ran on the legacy engine instead, and leaving the Agent unchanged was a valid decision. That engine is gone, so the same advisory now describes a refusal. If you have Agents saved before this change that you never revisited, check them: an advisory you previously ignored is now a failed dispatch.

The runtime lane cannot host an Agent whose configuration includes any of the following:

  • A wait-until or crawl step anywhere in the Agent’s inline Flow tools, top-level or nested inside a conditional. An Agent turn never parks on a durable pause, so every surface refuses the Agent with ineligible:wait-until or ineligible:crawl. Model the durable phase as a plain Flow instead, and dispatch that Flow on a surface with durable hosting.
  • Capabilities that also make a Flow exceed the runtime lane, such as a custom tool that requires a container-backed sandbox.

config.loggingPolicy: "off" and config.piiRedaction: "redact" are not in that list. Every surface that dispatches an Agent enforces both policies itself and declares them handled, so neither is ever a reason and neither produces this advisory.

Several configuration paths are a surface split: they run on some surfaces and are refused on others, and the advisory says so rather than telling you to remove them. Read the advisory message, which names the split for the path it reports.

ConfigurationRuns onRefused on
config.memory.enabledThe dispatch routes (POST /v1/dispatch, POST /v1/agents/:id/execute), the product surfaces (chat, AG-UI, Product API), messaging, and webhook — each resolves the memory bucket itselfBatch, evals, schedules, MCP, and A2A, which supply no memory wiring
config.tenancyStrategyThe dispatch routes and product surfaces, when the dispatch asserts a tenancyEvery unattended surface, and any dispatch that asserts no tenancy
config.sandbox.enabledEvery Agent surface — the dispatch routes, product surfaces, batch, evals, schedules, messaging, webhooks, Product MCP, and as a subagentVoice surfaces, and an Agent invoked from an execute-agent Flow step
config.temporal with injectElapsed, groundNow, or elapsedThresholdSecondsMessaging, where the elapsed-time notice is applied. Also batch, evals, schedules, webhooks, Product MCP, and detached subagent runs — these fire without a previous message, so the notice has nothing to measure from and is simply not addedThe dispatch routes, product surfaces, voice, and an execute-agent Flow step. These carry a conversation, so an omitted notice would be a silent surprise; they refuse the Agent instead
config.tools.runtimeTools[] with toolType: "local" or "webmcp"The dispatch routes and the product surfaces that hold a live caller (chat, AG-UI, hosted page, the Chrome extension) — the caller’s page executes the tool and returns its outputNowhere. Batch, evals, schedules, webhook, messaging, A2A, and Product MCP have no caller, so they drop the tool before the turn and run without it, rather than refusing the Agent

Client-executed tools are the one split that never refuses. Dropping the tool keeps a multi-surface Agent working: the same Agent can carry a widget tool for its chat surface and still run its nightly schedule, which simply does not see that tool. Each unattended run records the drop by name in its customer log, and saving an Agent that gains such a tool while bound to those surfaces returns a CLIENT_TOOL_UNRUNNABLE_ON_SURFACE notice naming them. If the model calls a client tool anyway on a path where one survived, the run still fails closed with the existing pause backstop.

A config.temporal block that sets only timezone is not a split and produces no advisory at all: every Agent surface applies it as the Agent’s default time zone.

Durable pause steps are not a split. An Agent whose inline Flow tool contains a wait-until or crawl step is refused on every surface, because an Agent turn never parks on a durable pause. The advisory names the step with ineligible:wait-until or ineligible:crawl. To use a durable pause, move that phase into a plain Flow and dispatch the Flow on a surface with durable hosting. See Timeouts and long-running work.

The advisory identifies the configuration paths and ineligible:* codes. It uses only the saved configuration. It does not analyze a capability that Runtype resolves when you save the Agent instead of storing in the configuration.

The advisory message has this form:

This agent cannot run: the runtime agent lane is the only agent engine, and it does not support: config.artifacts (config.artifacts). The legacy agent engine has been retired, so there is no opt-out that runs this execution anyway: change the request or the agent definition.

To fix it, remove or rewrite the flagged configuration path.

Some capabilities are retired

A small set of capabilities is retired: the runtime lane will not implement them. The advisory for one of these names the rejection explicitly, because no amount of waiting will make the capability arrive.

Today the retired set is one entry:

  • config.tools.perToolLimits, per-tool call limits on an Agent’s tool bag.

Interactive dispatch (POST /v1/dispatch and POST /v1/agents/:id/execute) and the product surfaces (chat, AG-UI, and the Product API) answer such a definition with HTTP 400 and code RUNTIME_AGENT_DEFINITION_FEATURE_UNSUPPORTED. That answer does not depend on the rest of the definition: a retired capability is decided first, so an Agent carrying both a retired capability and an unrelated unsupported one is still rejected for the retired one.

Every unattended surface — batch, evals, schedules, webhook, messaging, and Product MCP — fails the run closed.

A2A is the one exception, and it is not an exception to the outcome so much as to the gate: an ordinary Runtype Agent capability runs there as a virtual Flow through the Flow protocol, so this Agent-lane check does not decide it. Only a managed orchestrator on A2A is rejected the way an attended surface is.

The advisory for a retired capability has this form:

This agent uses a retired capability, so interactive dispatch (POST /v1/dispatch, POST /v1/agents/:id/execute) and the product surfaces (chat, AG-UI, Product API) REJECT it with RUNTIME_AGENT_DEFINITION_FEATURE_UNSUPPORTED rather than executing it, and every unattended surface (batch, eval, schedules, webhook, messaging, Product MCP) fails the run closed. Remove: config.tools.perToolLimits (config.tools.perToolLimits). The legacy agent engine has been retired, so there is no opt-out that runs this execution anyway: change the request or the agent definition.

To fix it, remove tools.perToolLimits from the Agent definition. Bound the loop with loopConfig.maxTurns or loopConfig.maxCost instead, or enforce the per-tool budget inside the tool itself.

Model advisory warnings

Runtype checks each prompt step’s model against the model catalog and against your account’s model settings. These checks describe the model selection rather than the Flow structure, so none of them blocks a save. They appear in warnings[], except NON_ROUTED_MODEL_VERSION, which appears in recommendations[].

A model the catalog does not know, such as a custom model you added yourself, is skipped by every check in this group.

Model is deprecated

Code: DEPRECATED_MODEL

The selected model is marked deprecated in the model catalog. A deprecated model still runs until its provider retires it, and it stops working without warning when that happens.

When the catalog knows a supported routed alias for the model’s family, the message names that alias as the replacement. When it does not, the message asks you to choose a supported model. Switch to the suggested alias, or pick a routed model such as claude-sonnet-5 or gpt-5.4-mini.

Model pins an explicit version

Code: NON_ROUTED_MODEL_VERSION (appears in recommendations[])

The step names a dated version or a provider-prefixed model ID instead of the routed alias for that family. A pinned ID keeps working, but it stays on one provider and one snapshot, so the step does not pick up the family’s later versions.

The recommendation names the alias:

Model "claude-sonnet-4-5-20250929" pins an explicit version or provider. Prefer the routed alias "claude-sonnet-4-5" so the platform can choose the best provider and stay current as models update.

Replace the pinned ID with the routed alias, such as claude-sonnet-4-5. Keep the pinned ID when you need one exact snapshot, for example to hold an Eval baseline steady.

Model does not support a capability the step enables

Code: MODEL_CAPABILITY_MISMATCH

The step enables reasoning on a model that does not support reasoning, or enables artifacts on a model that does not support tool use. Artifacts are produced through injected emit tools, so a model without tool use cannot produce them.

The warning names the model and the capability:

Model "gpt-4o-mini" does not support reasoning, but "reasoning" is enabled. Disable reasoning or choose a reasoning-capable model.

Turn the capability off, or select a model whose catalog entry supports it. For more information, see Using prompt steps.

Model rejects the temperature parameter

Code: TEMPERATURE_UNSUPPORTED

The step sets temperature on a model that does not accept the parameter. Runtype drops the value before it sends the request, so the setting has no effect and the model samples at its own default.

The warning names the model:

Model "claude-sonnet-5" does not support the "temperature" parameter, so the configured value is ignored at execution time. Remove the temperature setting or choose a model that supports it.

Remove the temperature setting, or choose a model that supports it.

Output cap is below the reasoning floor

Code: MAX_TOKENS_BELOW_REASONING_FLOOR

The step sets maxTokens below 1024 while reasoning is active. maxTokens caps reasoning tokens along with the visible answer, so reasoning can consume the whole budget. The step then returns an empty response with stopReason: "length" and still reports success: true.

Reasoning counts as active in three cases, and details.reasoningSource says which one fired:

  • requested: you enabled reasoning on a reasoning-capable model.
  • built-in: the model reasons by default, which covers the GPT-5 and o-series families.
  • built-in-not-disableable: you disabled reasoning, but the family has no off switch. The o-series floors at low effort, so it keeps spending the cap.

The warning names the model, the cap, and the fix:

Model "gpt-5" reasons by default, and "maxTokens" (64) caps reasoning tokens as well as the visible answer. Reasoning can use the whole budget and return an empty response. Raise maxTokens to at least 1024 or remove the cap.

Raise maxTokens to at least 1024, or remove the cap. Disabling reasoning fixes the problem only on a model that accepts an off switch.

Reasoning knob outside the model’s published range

Code: REASONING_KNOB_UNSUPPORTED

The step’s reasoning object sets a knob the model’s catalog entry does not accept. Each model publishes its own effort ladder and its own thinking-budget range, and a value outside them is either rejected by the provider or dropped before the request goes out. details carries { model, knob, requested, accepted }.

Three shapes trigger the warning:

  • An effort level outside the model’s ladder. The check reads the carrier the executor reads for that provider: reasoningEffort for OpenAI and xAI, effort for Claude models with adaptive thinking, and thinkingLevel for Gemini 3 and later.
  • budgetTokens on a Claude model with adaptive thinking. Adaptive thinking carries no token budget, so Runtype drops the value before it sends the request.
  • A thinking budget outside the model’s published minimum and maximum.

Each shape produces its own message:

Model "claude-sonnet-5" does not accept the reasoning effort "minimal". It accepts "low", "medium", "high", "xhigh", "max". Choose one of those or remove the setting.
Model "claude-sonnet-5" uses adaptive thinking, which carries no token budget, so "budgetTokens" (12000) is dropped before the request is sent. Remove it and set a reasoning effort level instead.
Model "gemini-2.5-flash" accepts a thinking budget of between 0 and 24576 tokens, but "thinkingBudget" is 40000. Adjust it to the accepted range or remove the setting.

Set the knob to a value the model publishes, or remove it. In the dashboard, reopening the model’s Reasoning controls and picking a level rewrites the setting for you, because the editor offers only the values that model accepts.

The check stays silent in four cases: the catalog makes no claim about the model’s knobs, reasoning is the boolean true form, which names no specific knob, reasoning.enabled is false, and the step is disabled. For more information, see Using prompt steps.

Model is not enabled for the account

Code: MODEL_NOT_ENABLED

The model is turned off in your account’s model settings, or it is not configured and no platform key or provider key can run it. The step fails at dispatch rather than at save time.

The warning names the model and the reason:

Model "gpt-5.4-mini" is disabled in this account's model settings. Enable it or choose an enabled model.

Enable the model in model settings, add a provider API key for its provider, or select a model your account can run. This check runs only when you validate as an authenticated user. For more information, see Managing AI models.

Approval gate is enforced on attended surfaces only

Code: APPROVAL_ENFORCED_ON_ATTENDED_SURFACES_ONLY

A prompt step sets config.tools.approval.require to ask a person to approve named tools before they run. Runtype enforces this gate only on execution hosts that can present the prompt and resume the run. These include interactive dispatch (POST /v1/dispatch and the dashboard Run panel) and Agent execution.

Other surfaces cannot ask a person to approve the prompt. These surfaces include schedules, webhooks, batch, evals, messaging, MCP, and A2A. On these surfaces, Runtype refuses the run with ineligible:surface-approval-pause. The gated tools do not run without a decision. Runtype reports this warning at save time.

Client Chat and chat widgets are a separate fail-closed case. They can pause for a browser-side client tool and resume that same visitor’s run through POST /v1/client/resume, but they cannot hand a human approval request to the Product owner and return the approved result to the visitor. When a Client Chat request can reach an approval-gated Flow or Agent, Runtype rejects it before execution with HTTP 501 and code APPROVAL_MODE_UNSUPPORTED.

The warning message names the split, and details carries both surface lists:

Step "Assistant" arms a human approval gate (config.tools.approval.require), which is enforced only on attended surfaces that can present the prompt and resume the run: interactive dispatch and agent execute. Client chat rejects approval-gated execution because it has no secure continuation channel back to the visitor. The other unattended surfaces (schedule, webhook, batch, eval, messaging, mcp, a2a) refuse the run (ineligible:surface-approval-pause): no engine answers the prompt without an attended client, so the flow does not run there at all. If it must run unattended, remove the approval requirement from this step.

Runtype reports this warning only for a top-level prompt step with an effective require: true or a non-empty list of tool names. An approval block that sets only timeout, requestReason, or choices shapes the prompt but does not create an approval gate. It does not trigger the warning.

To fix this warning, keep the Flow on attended surfaces, or remove the approval requirement from the step if the Flow must run unattended. The approval-gated Agent warning identifies Agent bindings that cannot answer a gate. This warning does not block a save.

Approval-gated Agent bound to a surface or mode that cannot answer

Code: APPROVAL_UNANSWERABLE_ON_SURFACE

An Agent that requires human approval before certain tools run needs a person who can answer the prompt. It also needs an execution mode that can resume afterward. If you bind that Agent to an unattended surface, or bind a single-pass Agent to Slack or Telegram, the approval-gated execution cannot continue. Runtype reports this warning while you edit the binding instead of at dispatch.

An Agent requires approval when it sets tools.approval.require, marks an individual tool as requiring approval, or binds a Skill whose tools expand the approval surface.

The following surface types have no approval-answering path: chat, webhook, schedule, email, discord, whatsapp, and generic messaging. Client Chat fails before execution with HTTP 501 and code APPROVAL_MODE_UNSUPPORTED instead of leaving an approval pending.

Slack and Telegram can show native Approve and Deny controls. SMS and iMessage have no buttons, so the person sends the decision in a reply. For more information, see Approvals on messaging surfaces. An ordinary Runtype Agent on any of these four surfaces needs a multi-turn Agent Loop run with a durable checkpoint to continue after the decision arrives. Runtype reports this warning for an approval-gated Runtype Agent when loopConfig.maxTurns is absent or set to 1. The warning stops after you enable Agent Loop with at least 2 turns. A Managed external Agent is compiled into an effective Runtime Agent Loop and does not need an authored loopConfig; Telegram, SMS, and iMessage still require their documented inbound verification credentials.

Runtype reports this warning in three places:

  • Product validation (validate_product or POST /validate) reports it in warnings[], with path pointing to the binding that triggered it, such as surfaces[2].routes[0].capabilityId.
  • Adding or updating a surface item through the REST endpoint or the add_surface_item MCP tool returns it in warnings[].
  • Saving an Agent returns it in _warnings. This case fires only when you first add an approval requirement to an Agent that is already bound to unattended surfaces. The message names those surfaces.

The validation message names the Agent, surface, and surface type:

Agent "Order triage" requires human approval for some tools, but the "Event intake" surface is a webhook surface with no one to ask. Runtype refuses the run before the agent starts, so no gated tool executes.

For a single-pass Slack, Telegram, SMS, or iMessage Agent, the message explains that the surface can resume approvals only for Agent Loop Agents.

This warning does not invalidate the binding: valid remains true and the save succeeds. One Agent can serve both a chat surface and a webhook surface in the same Product. Dispatch still determines whether each binding can run.

To fix the warning, remove the approval requirement from the Agent. For an ordinary Runtype Agent on Slack, Telegram, SMS, or iMessage, enable Agent Loop with at least 2 turns. A Managed external Agent already uses an effective Runtime Agent Loop. Do not move an approval-gated capability to Client Chat; that host rejects the request with APPROVAL_MODE_UNSUPPORTED.

Approval-gated Agent answered by reply text

Code: APPROVAL_REPLY_CAPTURE_ON_SURFACE (appears in recommendations[], not warnings[])

This recommendation covers an approval-gated Agent Loop Agent bound to an SMS or iMessage surface. The pairing works, but these transports have no approve or deny buttons, so the person sends the decision in a reply. An unclear reply prompts another request instead of authorizing an action. This recommendation identifies that the surface uses text replies instead of explicit controls.

Runtype reports a recommendation because the approval is answerable and the execution continues. The recommendation appears in product validation, when you add or update a surface item, and when you save an Agent. Keep approval-gated tools on this surface limited to low-risk actions. You can also bind the capability to a surface with explicit Approve and Deny controls, such as Slack or Telegram, or run it through the authenticated dashboard Run interface. For more information about reply keywords and expiry behavior, see Approvals on messaging surfaces.

Next steps

Continue with these related topics: