Skip to navigation

Flow validation warnings

Runtype checks a Flow’s configuration for mistakes that pass schema validation but fail, or produce a silently wrong result, at runtime. This page lists the codes those checks report most often, what triggers each one, and how to fix it. It also covers the advisories that Agent, Product, and Surface saves return for configurations that cannot run.

How validation results are reported

Validation sorts each issue into one of three lists:

  • errors[]: the configuration can’t be saved. A save that has any error fails.
  • warnings[]: the Flow saves, but the configuration predicts a runtime failure or a silently wrong result.
  • recommendations[]: the Flow saves and runs as written. The issue is an optimization or a design improvement.

An Agent save returns its advisories in the save response’s _warnings field instead. Each section on this page names the list that its code appears in.

A validation result has this shape:

{
"valid": true,
"errors": [],
"warnings": [
{
"code": "UPSERT_RECORD_SOURCE_NOT_JSON",
"message": "Source variable \"analysis_result\" is set by prompt step ...",
"path": "flowSteps[3].config.sourceVariable",
"step": { "index": 3, "name": "Save analysis", "type": "upsert-record" }
}
],
"recommendations": [],
"context": {
"authenticated": true,
"accountChecksPerformed": true,
"accountChecksSkipped": false
}
}

valid is true when errors is empty. Read code to identify the problem and message for the suggested fix. The path and step fields identify the step that triggered the issue. An issue about the whole Flow has no step.

Where you see validation results

You get the full result, with all three lists, from these places:

  • The Flow editor in the dashboard. The editor re-checks the Flow as you edit, and the toolbar shows a count of errors, warnings, and recommendations. Click the count to open Issues in this flow. Each item shows the message, the step name (or Whole flow), and the code. Click an item to select the step that it names.
  • The validate endpoint, POST /v1/public/flows/validate.
  • The SDK validate() method on a Flow builder.
  • The validate_flow MCP tool.

When you create or update a Flow through the API (POST /v1/flows or PUT /v1/flows/{id}), Runtype runs the same checks. A successful save returns only the Flow, without warnings or recommendations. To see warnings for a Flow that you save through the API, call the validate endpoint first.

A create or update that has errors returns HTTP 400. The errors are in details, not in errors, and step names the step of the first error:

{
"error": "Validation Error",
"step": { "name": "Process items", "type": "loop", "order": 2 },
"details": [
{
"code": "NESTED_LOOP_UNSUPPORTED",
"message": "Nested loop steps are not supported. Put the inner work in the same bounded loop or a called flow.",
"path": "flowSteps[2].config.steps",
"step": { "index": 2, "name": "Process items", "type": "loop" }
}
],
"warnings": [],
"recommendations": []
}

Validate a Flow before you save it

The validate endpoint checks a Flow definition without creating it. Send the Flow’s name and flowSteps:

cURL
curl -X POST https://api.runtype.com/v1/public/flows/validate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Summarize input",
"flowSteps": [
{
"name": "Draft response",
"type": "prompt",
"config": {
"model": "claude-sonnet-5",
"userPrompt": "Write a concise summary of {{input}}",
"outputVariable": "summary"
}
}
]
}'

Replace YOUR_API_KEY with your Runtype API key. A body that is not valid JSON returns HTTP 400 with the code INVALID_JSON.

With the TypeScript SDK, build the Flow and call validate():

TypeScript SDK
import { Runtype } from '@runtypelabs/sdk'
Runtype.configure({ apiKey: process.env.RUNTYPE_API_KEY })
const result = await Runtype.flows
.virtual({ name: 'Summarize input' })
.prompt({
name: 'Draft response',
model: 'claude-sonnet-5',
userPrompt: 'Write a concise summary of {{input}}',
})
.validate()
for (const issue of result.warnings) console.warn(issue.code, issue.message)

Checks that need authentication

The Authorization header is optional on the validate endpoint. Without it, Runtype runs every check that reads only the Flow definition and skips the checks that look up resources in your account. The skipped checks include TOOL_NOT_FOUND, FLOW_REFERENCE_NOT_FOUND, AGENT_NOT_FOUND, QUESTION_SET_UNRESOLVED, and MODEL_NOT_ENABLED.

The context object reports what ran:

  • authenticated: true when the request carried valid credentials.
  • accountChecksPerformed: true when Runtype checked the Flow’s references against your account.
  • accountChecksSkipped: true when the Flow references account resources but the request was not authenticated.

When Runtype skips account checks for a Flow that references account resources, warnings[] also contains ACCOUNT_CONTEXT_UNAVAILABLE. To run those checks, send the request with your API key.

Variable reference warnings

Codes: UNDECLARED_VARIABLE_REFERENCE, MALFORMED_VARIABLE_REFERENCE, and UNKNOWN_VARIABLE_NAMESPACE (warnings)

These warnings report a template reference that can’t resolve at runtime:

  • UNDECLARED_VARIABLE_REFERENCE: no earlier step’s output variable and no Flow input has the referenced name. details.availableVariables lists the names that are declared at that point in the Flow.
  • MALFORMED_VARIABLE_REFERENCE: a reference uses single braces, such as {customerName}, instead of {{customerName}}.
  • UNKNOWN_VARIABLE_NAMESPACE: a reference uses a prefix other than flow: or secret:.

For causes and fixes, see Reference warnings.

Structural errors that block a save

The following codes report a Flow structure that Runtype can’t save. Each appears in errors[], except where the table says otherwise.

CodeCauseFix
SCHEMA_VALIDATION_FAILEDA step’s config does not match the schema for its step type. path names the field.Correct the field that path names.
CONTEXT_STEP_INVALIDA non-prompt step’s configuration can’t be normalized, such as a missing required field.Follow the message, which names the field.
DUPLICATE_STEP_IDTwo steps, including nested steps, share an id.Give each step a unique id.
NESTED_LOOP_UNSUPPORTEDA loop step contains another loop step.Do the inner work in the same loop, or call a separate Flow.
CONDITIONAL_NESTING_TOO_DEEPConditional steps nest more than 10 levels deep.Flatten the branches.
SLOW_TRIGGER_INVALID_AFTER_MSA slow fallback trigger has no positive afterMs.Set afterMs to the duration cap in milliseconds.
RUNTIME_TOOLS_INVALIDA step’s config.tools.runtimeTools entry is invalid.Follow the message, which names the problem.
SUBAGENT_CONFIG_INVALID and SUBAGENT_CONFIG_ESCALATIONsubagentConfig is malformed, or its toolPool lists a tool that the parent step doesn’t have.Fix the field that the message names. Add each pool tool to the step’s toolIds or runtimeTools, or use a wildcard entry that ends in :*.
CODE_MODE_CONFIG_INVALID and CODE_MODE_CONFIG_ESCALATIONcodeModeConfig is malformed, lists a local tool, or lists a tool that the parent step doesn’t have.Fix the field that the message names, and remove local tools from the pool.
CODE_STEP_LOCAL_TOOL_UNSUPPORTEDA transform-data step attaches a local tool. Code runs without a client to call back to.Remove the local tool, or use a server-side tool.
CODE_STEP_TOOL_NAME_COLLISIONTwo tools attached to a transform-data step resolve to the same name.Rename one of the tools.
TOOL_NOT_FOUND, FLOW_REFERENCE_NOT_FOUND, and AGENT_NOT_FOUNDA step references a Tool, Flow, or Agent that doesn’t exist in your account. Runs only for authenticated validation.Correct the ID or name.
DUPLICATE_STEP_ORDERTwo steps share an order value. Appears in warnings[].Give each step a unique order.
NO_ENABLED_STEPSEvery step in the Flow is disabled. Appears in warnings[].Enable at least one step.
UNKNOWN_PROMPT_TOOLS_FIELDconfig.tools contains a key that Runtype ignores. The message suggests the correct key when it recognizes a common mistake. Appears in warnings[].Rename or remove the key.

List Records output uses object access

Code: LIST_RECORDS_OBJECT_ACCESS (warning)

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.metadata.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.metadata.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 Migrate from retrieve-record and Using record steps (get/list/upsert).

Record field read outside metadata

Code: RECORD_FIELD_PATH (warning)

A get-record step returns one record object, and a list-records step returns an array of them. A record has id, name, type, metadata, createdAt, updatedAt, ownerId, and schemaValid at the top level. The fields that you write live under metadata. A reference such as {{lead.score}} resolves to undefined at runtime.

The warning names the field and gives the corrected path, for example {{lead.metadata.score}} or {{leads.0.metadata.score}}. The warning does not block a save.

Deprecated retrieve-record step type

Code: DEPRECATED_STEP_TYPE (warning)

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 (warning)

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 }.

Upsert-record source variable is a template

Code: UPSERT_SOURCE_TEMPLATE (warning)

An upsert-record step’s sourceVariable is a variable name, such as classification. The step does not substitute templates in this field, so a value such as {{classification}} is looked up literally and the run fails with Source variable '{{classification}}' not found in context. Use the bare name. The warning’s details.suggestion gives it.

Fetch step failure feeds a later step

Code: FETCH_CLASS_SWALLOWING_FEED (warning)

A fetch-url, api-call, crawl, or search step has no errorHandling setting, and a later transform-data or upsert-record step reads its output. With errorHandling unset, a failed request continues with an empty result. The later step then runs over zero data as if the API returned nothing, and can write empty or wrong Records.

The warning message names both steps and the fix:

Step "Fetch orders" (api-call) has no errorHandling set, so a failed fetch silently produces an empty result (default continue) — and its output "{{orders}}" is read by "Save orders" (upsert-record). An empty fetch flows through as if the API legitimately returned no rows, which can drive downstream logic, analytics, or record writes over zero data. Either set errorHandling to "fail" on "Fetch orders" (recommended when its output feeds further logic), or set it to "continue" explicitly if an empty result is expected.

Set errorHandling.onError to fail on the fetch step when later steps depend on its data. If an empty result is expected, set errorHandling.onError to continue explicitly, and the warning stops. For more information, see Using fetch-url and api-call steps.

Transform-data language does not match the sandbox provider

Code: TRANSFORM_LANGUAGE_PROVIDER_MISMATCH (warning)

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

No environment can run that pair. 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.

Transform-data script calls fetch with network access off

Code: SANDBOX_NETWORK_ACCESS_DISABLED (warning)

A transform-data script calls fetch(), but the step’s network access is off. The execution environment blocks outbound requests while network access is off, so the request fails at runtime.

The warning also applies to QuickJS (Legacy), which can’t make outbound requests. On Cloudflare Worker (Default) or Runtype Sandbox, set Network Access to Unrestricted, or to Allowed Hosts Only with the host in Allowed Hostnames. In a Flow definition, set networkAccess to "on" or to an allowedHostnames list that includes the host. You can also switch to Daytona Sandbox, which doesn’t use this setting. For more information, see Control network access.

Transform-data tools on an environment that cannot call them

Code: TRANSFORM_POOL_TOOLS_ON_CONTAINER_PROVIDER (warning)

A transform-data step attaches tools through config.tools, but its execution environment is not Cloudflare Worker. Only Cloudflare Worker lets a script call attached tools. On other environments, every call to one of those tools fails at runtime, and because transform-data continues on error by default, the step can report success with an empty output.

Switch the step to Cloudflare Worker, or remove the tools.

Transform-data script issues

Codes: CODE_-prefixed codes and MISSING_LANGUAGE

Runtype also checks the script of a transform-data step when you save it. Each issue that the script check finds appears with a CODE_ prefix, such as CODE_SYNTAX_ERROR or CODE_SHADOWED_FLOW_VARIABLE, in errors[] or warnings[] depending on its severity. details.line and details.column locate the problem in the script, and the message repeats them.

Two common script errors are:

  • CODE_SYNTAX_ERROR: the script does not parse. Fix the syntax at the reported line and column.
  • CODE_SHADOWED_FLOW_VARIABLE: the script declares a variable with the same name as a Flow variable. Runtype makes each Flow variable available to the script under its own name, so the redeclaration fails when the script loads. Rename your variable, or read the value through input.

MISSING_LANGUAGE is a warning for a step that uses Runtype Sandbox without a language. The step runs as JavaScript. Set language to javascript, typescript, or python to make the choice explicit.

For more information, see Using transform-data steps.

Store-vector source is unresolved

Code: STORE_VECTOR_SOURCE_UNRESOLVED (warning)

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 (error) and STORE_VECTOR_SOURCE_MODELLESS (recommendation)

Set vectorsSource to the whole generate-embedding output, such as emb, not emb.embedding, so the embedding model and dimensions travel with the vector. A store-vector step has no embeddingModel setting of its own. A pgvector store needs the model name, because vector search finds a row only when the row’s model matches, so a store whose source carries no model fails at execution time instead of saving the row.

Validation reports two source shapes that drop the model:

  • A .embedding path into a generate-embedding output, such as emb.embedding. This path returns only the vector, so the step always fails. Validation reports STORE_VECTOR_MODEL_MISSING as a blocking error.
  • A transform-data output. Your script decides the shape of this output and might keep the model, so validation reports STORE_VECTOR_SOURCE_MODELLESS as a recommendation.

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.

To fix a transform-data source, spread the whole generate-embedding output in the script, such as { ...embeddingOutput, ...extras }, so model and dimensions stay in the result. Don’t fill in model and dimensions with unrelated values, such as the name of the text model. The store accepts them and overwrites a correct model name.

The following rules also apply:

  • Vectors from other sources. For a vector that generate-embedding did not produce, supply an object with an embedding array, a model string, and a numeric dimensions.
  • Batch stores. Each batch item needs its own non-empty embedding_model. A batch-mode generate-embedding step adds it automatically. When a batch store reads its items from a transform-data output, validation reports STORE_VECTOR_SOURCE_MODELLESS and names the required per-item key.
  • Error handling. With errorHandling unset, a store without a model reports a failure and writes nothing. An explicit continue substitutes the step’s defaultValue, or the step’s standard error result (destination, storedCount: 0, error) when no default is set. An explicit fail stops the Flow. In a batch store, an item without an embedding_model fails with No embedding model, and the valid items still store.
  • Other destinations. These checks apply only to stores that target pgvector. Weaviate and Vectorize accept a bare vector.

A reference to the whole generate-embedding output doesn’t trigger either code. If no earlier step produces that variable, Runtype reports STORE_VECTOR_SOURCE_UNRESOLVED instead.

Tool call strategy required on a multi-step prompt

Code: TOOL_STRATEGY_REQUIRED_MULTISTEP (warning)

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 intent policy on a flow prompt step

Code: TOOL_INTENT_POLICY_AGENT_ONLY (warning)

A prompt step sets tools.intent. Only an Agent’s durable run records tool intents, so a flow prompt step ignores the setting.

The validation message explains the scope:

tools.intent applies to agents on a durable host; a flow prompt step writes no tool intents, so it has no effect here.

Remove tools.intent from the prompt step, or set it on the Agent that calls the tool. For more information, see Require a committed intent before a tool runs.

Tool call strategy none with tools attached

Code: TOOL_STRATEGY_NONE_WITH_TOOLS (warning)

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.

Provider-native tools from more than one provider

Code: PROVIDER_TOOLS_MIXED_OWNERS (warning)

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), builtin:xai_web_search and builtin:xai_x_search (xAI), and builtin:google_search (Google). A prompt step that attaches such tools is routed to the provider that owns them. One request reaches one provider. When a step attaches provider-native tools from two providers, only one provider’s tools run, and the other provider’s tools are dropped or sent as inert function tools with no error.

Runtype routes the step to the first provider whose tools need a direct connection to that provider. Anthropic, OpenAI, and xAI tools need a direct connection, and builtin:google_search also works through the Vercel AI Gateway. When every attached provider works through the gateway, the first provider-native tool in toolIds decides the route.

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

The validation message names the provider that runs 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, 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.

Optional parameter interpolated into an external tool template

Code: OPTIONAL_PARAM_IN_TOOL_TEMPLATE (warning)

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 stays in the request as its literal {{parameter}} text, so the outbound call is malformed and the third-party service usually answers with a confusing 4xx error.

This warning fires when the templated name is declared in parametersSchema.properties, is absent from parametersSchema.required, and the template has no fallback. Runtype reports one warning 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 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.

String tool parameter that carries embedded JSON

Code: TOOL_PARAM_EMBEDDED_JSON_STRING (recommendation)

A runtime tool’s parametersSchema declares a property that admits a string (type: "string", a nullable type: ["string", "null"], or an anyOf / oneOf with a string member) whose name or description says the model must write a JSON document inside the string. Names such as config, payload, json, or any name ending in json count, and so does a description that mentions a JSON string, stringified JSON, or JSON-encoded data. Models drop escapes when they nest JSON inside a JSON string. At runtime the call arrives as malformed JSON, Runtype rejects it as an invalid tool input, and the turn is wasted. Runtype repairs syntax-only defects such as trailing commas, unbalanced brackets, and unescaped inner quotes when it can, and logs each repair, but the schema is the durable fix.

This recommendation identifies a non-blocking design issue. The Flow runs as before. details.parameter names the property and details.signal reports whether the name or the description suggested embedded JSON.

The recommendation message names the tool and the parameter:

Runtime tool "validate_workflow" declares "config" as a string parameter, but its name suggests the model must write a JSON document inside the string. Models drop escapes when they nest JSON inside a JSON string, which surfaces at runtime as an invalid tool input: the call is rejected and the turn is wasted. Runtype repairs syntax-only defects when it can, but the durable fix is in your schema: declare "config" as { "type": "object" } with its properties so the model emits structured JSON, and stringify it inside the flow or tool wherever the string form is needed.

Declare the property as { "type": "object" } with its own properties so the model emits structured JSON, and stringify it inside the flow or tool wherever the string form is needed. Properties of any other type, and string properties whose name and description do not mention JSON, are not reported.

This recommendation is not limited to Flow validation. Saving a tool through POST /v1/tools or PUT /v1/tools/{id} runs the same check over the tool’s own parametersSchema, and a flagged property comes back on the saved tool as validation.recommendations[] with the path parametersSchema.properties.<name>. The save always succeeds.

Conditional compares an unquoted template against a string

Code: CONDITION_UNQUOTED_TEMPLATE_COMPARISON (warning)

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 (recommendation)

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.

Prompt input is only an upstream prompt output

Code: PROMPT_INPUT_UNGUARDED_UPSTREAM_OUTPUT (recommendation)

A prompt step’s whole user prompt is one {{var}} reference to an earlier prompt step’s output, with no fallback. A prompt step that fails or stalls writes an empty string under the default continue behavior, so this step would have no input left. Runtype fails the step before it reaches the model, with an error naming the empty variable, so the run reports a failure one step away from the cause.

The recommendation message names the upstream step and the fix:

Step "Compose" sends only "{{analysis}}", the output of prompt step "Analyze". A prompt step that fails or stalls writes an empty string under the default continue behavior, so this step would have no input left and fails before it reaches the model, one step away from the cause. Give the reference a fallback such as "{{analysis ?? 'No input was produced.'}}", add instructions around it, or set errorHandling to "fail" on "Analyze" so the run stops at the real cause.

A step that also attaches a conversation history with previousMessages fails differently, and its message says so: the emptied reference contributes nothing, the step still runs on the conversation history alone with the intended input missing, and it fails before it reaches the model only when that history is empty. The details.carriesConversationHistory field reports which outcome applies.

Give the reference a fallback, add instructions around it, or set errorHandling to fail on the upstream step. A reference that already carries a fallback, sits beside other text, or points at a step that is not a prompt step is not reported.

Template references a reserved identity field

Code: RESERVED_IDENTITY_VARIABLE (recommendation)

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 (recommendation)

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

Act on this recommendation when a preview alias starts writing memory that live traffic must not see. Append the pointer to memory.profileTemplate in one of these ways:

  • {{_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 turns memory off 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 when the setting that applies names the pointer: the template on an ordinary Agent, or 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 set of options, including what happens on rollback and when you retarget an alias, see Memory namespaces and release pointers.

Pointer-keyed memory template has no fallback

Code: MEMORY_POINTER_TEMPLATE_UNGUARDED (recommendation)

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 turns memory off for an execution whose template does not resolve. An Agent whose template is meant to isolate previews therefore runs with memory off on every request that names no version.

Give the reference a fallback so requests without a selector use 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-readable label: free text, not unique across versions, and absent on an unlabeled version. Key on {{_agent.versionId}} and keep the label for display.

Memory pointer scope has no effect

Code: MEMORY_POINTER_SCOPE_IGNORED (warning)

An Agent sets memory.pointerScope to "alias" or "version", but has no tenancyStrategy. Runtype adds a pointer segment to the memory namespace only for an Agent with a Tenancy Strategy. Every other Agent resolves memory.profileTemplate, which never reads pointerScope, so an Agent configured for per-pointer isolation keeps one shared bucket.

Fix it in the direction you intended:

  • 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 you intended one shared bucket, remove pointerScope.

For the full set of options, see Memory namespaces and release pointers.

Runtime tool claims a reserved tool name

Code: RESERVED_TOOL_NAME (error, 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 also runs offline, so the SDK and the dashboard report it before you save. For more information, see Agent tools.

Custom tool egress settings are malformed

Code: CUSTOM_TOOL_EGRESS_CONFIG_INVALID (error, blocks the save)

A custom runtime tool’s networkAccess or secrets entry can’t be used as written. For example, networkAccess isn’t 'off', 'on', or a non-empty allowedHostnames list, a secret key isn’t a bare managed secret name, the same secret appears twice, the Tool declares more than 10 secrets, or a hostnamePattern isn’t one public host. *, *.com, public suffixes such as *.co.uk or *.github.io, IP addresses, localhost, and .internal names are refused.

Secret "GITHUB_TOKEN" hostnamePattern "*" is not a single public host. Name the API host (api.github.com) or one domain's subdomains (*.github.com); public suffixes (*.co.uk, *.github.io), IP literals, localhost and internal names are refused.

Fix the field that the error path names. Write the secret key as GITHUB_TOKEN, not {{secret:GITHUB_TOKEN}}. For the fields, see Call an API from custom code.

Custom tool asks for egress its environment can’t provide

Code: CUSTOM_TOOL_EGRESS_PROVIDER_UNSUPPORTED (error, blocks the save)

A custom runtime tool sets networkAccess or secrets on an execution environment that can’t honor it. QuickJS has no network access and no egress proxy. Daytona Sandbox manages its own egress, so it can’t enforce an allowedHostnames list or inject secrets.

sandboxProvider "quickjs" has no fetch and no egress proxy, so networkAccess cannot be enabled. Set sandboxProvider to "cloudflare-worker" (JavaScript) or "runtype-sandbox" (JavaScript, TypeScript, Python).

Set sandboxProvider to cloudflare-worker or runtype-sandbox, or remove the egress settings.

Custom tool secret goes to a host its network access blocks

Code: CUSTOM_TOOL_SECRET_HOST_NOT_ALLOWED (error, blocks the save)

A custom runtime tool declares a secret for a host that its own networkAccess doesn’t allow. The request that carries the secret would be blocked, so the secret could never be used. An omitted networkAccess means 'off', so it allows no host.

Secret "GITHUB_TOKEN" is sent to "api.github.com", but networkAccess does not allow that host. Add it to networkAccess.allowedHostnames.

Add the secret’s hostnamePattern to networkAccess.allowedHostnames.

Flow tool writes into a reserved system scope

Code: FLOW_TOOL_OUTPUT_VARIABLE_RESERVED (error, blocks the save)

A flow-type runtime tool sets config.outputVariable to a name starting with _. Names in that namespace are Runtype’s system scopes (_flow, _user, _execution, _record), and they carry caller identity into every template and step in the run. A flow tool writing its result there would overwrite that identity.

The error message names the variable:

Flow tool output variable "_flow" is reserved: names starting with "_" are system scopes (_flow, _user, _execution, _record)

Pick a name outside the _ namespace, such as enrichment, or clear the field to keep the tool’s default result envelope. The same rule applies to a flow tool attached to an Agent. This check also runs offline, so the SDK and the dashboard report it before you save. For more information, see Flow variables and templates.

Tool-call step points at an unknown catalog tool

Code: TOOL_CALL_STEP_UNKNOWN_CATALOG_TOOL (error, 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 runs at a fixed point in the Flow with the parameters that 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.

Tool-call step ignores errorHandling

Code: TOOL_CALL_ERROR_HANDLING_IGNORED (warning)

A tool-call step sets config.errorHandling.onError. Tool-call steps read only their own config.onError field (fail, continue, or retry), so errorHandling.onError has no effect. A failed tool call is handled as config.onError. When onError is unset, the value is fail, and a failed tool call fails the Flow execution. details.effectiveOnError names the value that applies.

Move the value to config.onError and remove errorHandling. fallback has no tool-call equivalent: set config.onError to continue and branch on the { error, result: null } output instead.

Extract step field map is invalid

Code: EXTRACT_FIELDS_INVALID (error, blocks the save)

An extract step’s config.fields breaks a field rule: a name the model refuses, an unknown type, an option the type does not take (for example decimals on a text field), or a required option missing (items on a list; min and max on an estimate; min, max, horizon, and interval on a forecast). One issue is reported for each broken rule at config.fields.<name>, and details.code names the rule.

Fix the named field. See Extraction for every type and its options.

Extract step names a model that is not an extraction model

Code: EXTRACT_MODEL_UNKNOWN (error, blocks the save)

An extract step’s config.model is not in the extraction model catalog. A chat model or a decision model cannot run the step.

Omit model to use the default (@cf/cloudflare/clairic), or pick an ID from GET /v1/extraction-models.

Extract step lists too many images

Code: EXTRACT_IMAGES_TOO_MANY (error, blocks the save)

An extract step lists more config.images than the model reads. details.maxImages holds the limit (4 for Clairic).

Keep at most that many images, or split the pages across two steps.

Extract step has nothing to read

Code: EXTRACT_STATE_EMPTY (warning)

An extract step’s config.state is empty and no images are configured. At run time, an empty resolved text with no images is an input-contract failure.

Put the text to read in state, for example {{message}}, or add at least one image.

Extract field says nothing to the model

Code: EXTRACT_FIELD_UNDESCRIBED (recommendation)

A field has a one- or two-character name and no instructions. The field name is what the model sees, so such a field asks for nothing in particular.

Use a descriptive name such as order_id, or add one sentence of instructions.

Tool configuration references an unresolved question set

Code: QUESTION_SET_UNRESOLVED (error, blocks the save)

The judgment tool’s config.toolConfig.questionSetId (a tool-call step) or the matching entry of config.tools.toolConfigs (a prompt step) names a question set that cannot be resolved at save time. The check uses the same lookup that runs at execution time, so a reference that fails here would also fail the run: a judgment call never falls back to inline questions. This check runs only for authenticated validation.

One of three reasons produces the error, each with its own message:

Question set "qset_nonexistent" was not found for this account.
Question set "qset_draft_only" has no published version. Publish a version before referencing it.
Version 3 of question set "qset_live" does not exist or is not published. Reference a published version, or omit the version to use the current one.

Fix the reference:

  • Use list_question_sets (MCP) or GET /v1/question-sets to find a real question set ID for this account.
  • Call publish_question_set_version (MCP) or POST /v1/question-sets/{id}/versions/{version}/publish if the set exists but has no published version, or if the specific version you named is a draft.
  • Omit version to resolve the current published version instead of pinning to one that might not exist.

Execute-agent step uses a template variable for the Agent

Code: EXECUTE_AGENT_TEMPLATED_REF_UNRUNNABLE (warning)

An execute-agent step’s config.agentId is a template expression such as {{agentVar}} instead of a fixed Agent ID. Runtype can’t check an Agent that it only learns about at run time, so every surface refuses the whole Flow with 422 RUNTIME_LANE_INELIGIBLE (reason ineligible:unresolvable-agent-ref) 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 when you save, so the approval, client-tool, and sandbox checks still apply. The Flow-level RUNTIME_LANE_INELIGIBLE recommendation does not report a templated reference. This warning reports it instead.

An execute-agent step names an agent whose live alias is archived

Code: EXECUTE_AGENT_TARGET_NOT_DEPLOYED (warning)

An execute-agent step names an Agent in your organization that has archived its live release alias. A run that names no version selector can resolve the live alias, and then this step fails with AGENT_NOT_DEPLOYED partway through the Flow.

Deploy a version to the target Agent’s live alias and the warning goes away.

An Agent that was never published does not report this. Its first run without a selector deploys its current configuration as version 1, so the step runs. Agents owned by a person rather than an organization, and Agents of type external or claude_managed, keep running their current configuration and never report it either.

Record step writes an undeclared collection field

Code: RECORD_FIELD_NOT_IN_SCHEMA (warning)

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 without running the Flow: 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, or remove the field from the step’s updates or contentField. For more information, see Defining a schema.

Record step leaves a required schema field unmapped

Code: RECORD_REQUIRED_FIELD_UNMAPPED (recommendation)

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.

Durable pause exceeds its limits

Codes: WAIT_UNTIL_MAX_ATTEMPTS_CLAMPED (warning), WAIT_UNTIL_POLL_EXCEEDS_RESUME_TTL (error), and CRAWL_COMPLETION_TIMEOUT_EXCEEDS_RESUME_TTL (error)

A wait-until step or an asynchronous crawl step pauses the Flow while it waits. Validation checks that the pause fits the platform limits:

  • WAIT_UNTIL_MAX_ATTEMPTS_CLAMPED: poll.maxAttempts is greater than 1000. A durable poll runs at most 1000 attempts, so Runtype lowers the count to 1000. To cover the same time window, raise poll.intervalMs instead of adding attempts.
  • WAIT_UNTIL_POLL_EXCEEDS_RESUME_TTL: the poll’s worst-case duration (attempts multiplied by the per-attempt timeout, plus the interval between attempts) is longer than the 30-day limit for a paused Flow, less a one-hour reserve. A poll that outlives that limit can never resume, so the Flow would stop with no error and no output. Reduce poll.maxAttempts, poll.intervalMs, or poll.http.timeout, or split the wait across separate runs, such as a scheduled Flow that checks again.
  • CRAWL_COMPLETION_TIMEOUT_EXCEEDS_RESUME_TTL: the crawl’s completion timeout, including time spent on requests, exceeds the same limit. Runtype refuses such a crawl only after it has been submitted to the crawl provider, so fixing it at save time avoids paying for a crawl that can’t finish. Reduce the values that the message names, such as completionTimeoutMs or pollIntervalMs.

Each message states the computed duration and the values that drive it. For more information, see Timeouts and long-running work.

Flow uses a capability that cannot run

Code: RUNTIME_LANE_INELIGIBLE (recommendation)

Some Flow configurations can’t run on any surface. When you dispatch such a Flow, Runtype refuses it before any step runs: interactive dispatch returns 422 with the code RUNTIME_LANE_INELIGIBLE, and batch, eval, and schedule runs are refused the same way. This recommendation reports those configurations when you save, and names each one with a stable ineligible:* reason in details.reasons. It does not block the save.

The following configurations can’t run:

  • A top-level wait-until or crawl step on a surface that can’t pause a Flow and resume it later. Interactive dispatch can run these steps, and so can a Flow where the step sits inside a top-level conditional. Because save-time validation can’t know which surface you’ll use, the recommendation names the step on every save.
  • A custom tool configured for a container-backed sandbox, a language other than JavaScript, or the legacy code-mode setting. Custom tools run only JavaScript on Cloudflare Worker or QuickJS.
  • A transform-data step whose language and execution environment don’t match. The language and provider mismatch warning reports the same pairing. Python or TypeScript on Runtype Sandbox or Daytona Sandbox runs normally.
  • A vector step that Runtype can’t represent, such as a store-vector step with no vector ID source or a Weaviate destination. A non-default Vectorize index selected with vectorizeConfigId runs normally.
  • An unrecognized step type.

A wait-until pause can also fail after it starts. A human approval gate, per-dispatch client tools, or a browser-executed tool ahead of the pause doesn’t prevent the pause, but the run can’t resume without an attached client to answer. The resumed run fails with a message that explains the cause. To complete such a run, dispatch it again with a client attached, or remove the configuration that needs one.

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, outside interactive dispatch with durable hosting (ineligible:wait-until).

To run the Flow on every surface, remove or rewrite the flagged configuration. For example, configure a custom tool with the Cloudflare Worker or QuickJS sandbox and JavaScript.

On the Flow validation response, this recommendation’s path is flowSteps. Product and full product object (FPO) validation also reports it for an inline capability Flow, with path set to capabilities[N].flow. Runtype doesn’t 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 doesn’t by itself cause this recommendation. The surface decides whether an approval can be answered, so the approval enforcement warning covers it instead.

For authenticated validation, the check also includes the Agents that execute-agent steps reference. The recommendation fires when a referenced Agent has a configuration that can’t run, such as a custom tool that needs a container-backed sandbox. A reference to an Agent that doesn’t exist is reported as AGENT_NOT_FOUND instead, and a templated reference as EXECUTE_AGENT_TEMPLATED_REF_UNRUNNABLE. Unauthenticated validation reads only the Flow’s own steps.

Agent configuration that cannot run on some surfaces

Code: RUNTIME_AGENT_LANE_INELIGIBLE (Agent save advisory in _warnings)

When you save an Agent whose configuration can’t run on some or all surfaces, the save response includes this advisory in _warnings. The save succeeds, but a dispatch on an unsupported surface is refused. Claude Managed Agents and external Agents in Delegate mode run on their own execution paths, so this advisory doesn’t apply to them. An external Agent with skillOrchestration: "managed" does receive it. On a surface that can’t run its managed definition, execution fails instead of delegating directly to the external Agent.

An Agent can’t run on any surface when its configuration includes either of the following:

  • A wait-until or crawl step anywhere in the Agent’s inline Flow tools, top-level or inside a conditional. An Agent turn never pauses durably, so every surface refuses the Agent with ineligible:wait-until or ineligible:crawl. Put the long wait in a plain Flow instead, and dispatch that Flow on a surface that supports durable pauses. For more information, see Timeouts and long-running work.
  • A configuration that also makes a Flow unrunnable, such as a custom tool that needs a container-backed sandbox.

The logging policy setting config.loggingPolicy: "off", config.piiRedaction: "redact", and config.temporal never produce this advisory. Every surface that dispatches an Agent applies all three. That includes an execute-agent Flow step and a subagent. They have no previous turn, so groundNow adds the current time and injectElapsed adds nothing. Only the self-hosted runtime’s detached subagent tools still reject a temporal block.

Several settings run on some surfaces and are refused on others. For these, the advisory message names the surfaces instead of telling you to remove the setting. The following table lists them:

SettingRuns onRefused on
config.memory.enabledDispatch (POST /v1/dispatch, POST /v1/agents/:id/execute), product surfaces (chat, AG-UI, Product API), messaging, and webhooksBatch, evals, schedules, MCP, and A2A
config.tenancyStrategyDispatch and product surfaces, when the request asserts a tenancyBatch, evals, schedules, MCP, A2A, and any request that asserts no tenancy
config.sandbox.enabledDispatch, product surfaces, batch, evals, schedules, messaging, webhooks, Product MCP, and subagent runsVoice surfaces, and an execute-agent Flow step
config.tools.runtimeTools[] with toolType: "local" or "webmcp"Dispatch, and product surfaces with a live caller (chat, AG-UI, hosted page, Chrome extension)None. Other surfaces remove the tool and run without it.

The surfaces that refuse memory and tenancy don’t provide the memory or tenancy information that the setting needs.

Client-executed tools (local and webmcp) never cause a refusal. A surface without a live caller removes the tool before the turn, so one Agent can carry a widget tool for its chat surface and still run its nightly schedule without that tool. Each run that removes a tool records the tool name in its log. Saving an Agent that gains such a tool while it’s bound to those surfaces returns a CLIENT_TOOL_UNRUNNABLE_ON_SURFACE notice that names them. If the model calls a client tool on a path where one remains, the run fails.

The advisory names the configuration paths and ineligible:* codes from the saved configuration only. It doesn’t analyze a capability that Runtype resolves when you save the Agent instead of storing it 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.

Retired capabilities

A retired capability is one that Runtype doesn’t run on any surface. The advisory for a retired capability says so explicitly. The only retired capability is config.tools.perToolLimits, which sets per-tool call limits on an Agent’s tools.

Interactive dispatch (POST /v1/dispatch and POST /v1/agents/:id/execute) and the product surfaces (chat, AG-UI, and the Product API) reject such an Agent with HTTP 400 and the code RUNTIME_AGENT_DEFINITION_FEATURE_UNSUPPORTED. Runtype checks for a retired capability first, so an Agent that has both a retired capability and another unsupported configuration is rejected for the retired one. Batch, evals, schedules, webhooks, messaging, and Product MCP fail the run. On A2A, Runtype applies this check only to a managed orchestrator Agent, which A2A rejects in the same way as interactive dispatch.

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 (warning)

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 (recommendation)

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 that the message names. Keep the pinned ID when you need one exact snapshot, for example to hold an Eval baseline steady.

A model ID whose only difference from its family is the version spelling, such as gemini-3.8-flash against the family gemini-3-8-flash, is not a pin. Both spellings name the same model and route identically, so this recommendation never appears for one.

This recommendation also stays quiet when the ID names a provider you added your own key for, such as an Azure or OpenRouter model. Adding the key is how you choose the provider, so Runtype does not suggest routing the step away from it. A dated version ID still gets the recommendation, because a provider key says which provider runs the step, not which snapshot of the model.

Model does not support a capability the step enables

Code: MODEL_CAPABILITY_MISMATCH (warning)

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 (warning)

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 (warning)

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 model has no off switch, so it still reasons at a floor. details.disabledReasoningEffort names that floor:

    • The o-series floors at low effort.
    • A Gemini model with a thinking-level ladder floors at the lowest level its catalog entry lists. gemini-3.8-flash floors at low, and models that list minimal floor at minimal.
    • A Gemini budget-mode model with no off toggle floors at its minimum thinking budget, reported as a string such as "128-token thinking budget" (gemini-2.5-pro).

    Gemini models with an off toggle, such as gemini-2.5-flash and gemini-2.5-flash-lite, do turn thinking off, so disabling reasoning clears the warning on them.

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 whose catalog entry has an off toggle. On other models, disabling reasoning lowers it to the floor that details.disabledReasoningEffort names.

Reasoning knob outside the model’s published range

Code: REASONING_KNOB_UNSUPPORTED (warning)

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 field that Runtype sends to 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 (warning)

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 for authenticated validation. For more information, see Managing AI models.

Approval gate is enforced on attended surfaces only

Code: APPROVAL_ENFORCED_ON_ATTENDED_SURFACES_ONLY (warning)

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 surfaces that can present the prompt and resume the run: interactive dispatch (POST /v1/dispatch and the dashboard Run panel) and Agent execution.

Schedules, webhooks, batch, evals, messaging, MCP, and A2A can’t present an approval prompt. On these surfaces, Runtype refuses the run with ineligible:surface-approval-pause, and the gated tools don’t run without a decision.

Client Chat and chat widgets are a separate case. They can pause for a browser-side client tool and resume that visitor’s run through POST /v1/client/resume, but they can’t send an 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.

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

Code: APPROVAL_UNANSWERABLE_ON_SURFACE (warning)

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 runs as an Agent Loop without 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 runs as an Agent Loop. Don’t move an approval-gated capability to Client Chat, because Client Chat rejects the request with APPROVAL_MODE_UNSUPPORTED.

Approval-gated Agent answered by reply text

Code: APPROVAL_REPLY_CAPTURE_ON_SURFACE (recommendation)

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.

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: