> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# 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:

```json
{
  "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:

```json
{
  "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`**

```bash title="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`**

```typescript title="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`](#tool-configuration-references-an-unresolved-question-set), and [`MODEL_NOT_ENABLED`](#model-is-not-enabled-for-the-account).

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](/user-guide/flows/flow-variables-and-templates#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.

| Code                                                                | Cause                                                                                                                                                  | Fix                                                                                                                                            |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `SCHEMA_VALIDATION_FAILED`                                          | A step's `config` does not match the schema for its step type. `path` names the field.                                                                 | Correct the field that `path` names.                                                                                                           |
| `CONTEXT_STEP_INVALID`                                              | A non-prompt step's configuration can't be normalized, such as a missing required field.                                                               | Follow the message, which names the field.                                                                                                     |
| `DUPLICATE_STEP_ID`                                                 | Two steps, including nested steps, share an `id`.                                                                                                      | Give each step a unique `id`.                                                                                                                  |
| `NESTED_LOOP_UNSUPPORTED`                                           | A `loop` step contains another `loop` step.                                                                                                            | Do the inner work in the same loop, or call a separate Flow.                                                                                   |
| `CONDITIONAL_NESTING_TOO_DEEP`                                      | Conditional steps nest more than 10 levels deep.                                                                                                       | Flatten the branches.                                                                                                                          |
| `SLOW_TRIGGER_INVALID_AFTER_MS`                                     | A `slow` fallback trigger has no positive `afterMs`.                                                                                                   | Set `afterMs` to the duration cap in milliseconds.                                                                                             |
| `RUNTIME_TOOLS_INVALID`                                             | A step's `config.tools.runtimeTools` entry is invalid.                                                                                                 | Follow the message, which names the problem.                                                                                                   |
| `SUBAGENT_CONFIG_INVALID` and `SUBAGENT_CONFIG_ESCALATION`          | `subagentConfig` 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_ESCALATION`        | `codeModeConfig` 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_UNSUPPORTED`                                  | A 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_COLLISION`                                     | Two 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_FOUND` | A 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_ORDER`                                              | Two steps share an `order` value. Appears in `warnings[]`.                                                                                             | Give each step a unique `order`.                                                                                                               |
| `NO_ENABLED_STEPS`                                                  | Every step in the Flow is disabled. Appears in `warnings[]`.                                                                                           | Enable at least one step.                                                                                                                      |
| `UNKNOWN_PROMPT_TOOLS_FIELD`                                        | `config.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:

```text
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](/user-guide/flows/using-record-steps-upsert-retrieve#migrate-from-retrieve-record) and [Using record steps (get/list/upsert)](/user-guide/flows/using-record-steps-upsert-retrieve).

## 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](/user-guide/flows/using-record-steps-upsert-retrieve). 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:

```text
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:

```text
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:

```text
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](/user-guide/flows/using-fetch-url-and-api-call-steps#handle-errors).

## 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:

```text
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](/user-guide/flows/using-transform-data-steps#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](/user-guide/flows/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:

```text
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:

```text
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:

```text
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](/user-guide/flows/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:

```text
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](/user-guide/tools/runtime-tools#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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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](/user-guide/flows/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:

```text
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](/user-guide/settings/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:

```text
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:

```text
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](/user-guide/flows/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`](#memory-pointer-scope-has-no-effect) instead. For the full set of options, including what happens on rollback and when you retarget an alias, see [Memory namespaces and release pointers](/user-guide/agents/agent-versioning-and-publishing#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:

```text
{{_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](/user-guide/agents/agent-versioning-and-publishing#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:

```text
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](/user-guide/agents/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.

```text
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](/user-guide/tools/creating-custom-tools#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.

```text
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.

```text
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:

```text
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](/user-guide/flows/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:

```text
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](/user-guide/tools/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](/user-guide/flows/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:

```text
Question set "qset_nonexistent" was not found for this account.
```

```text
Question set "qset_draft_only" has no published version. Publish a version before referencing it.
```

```text
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:

```text
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](/user-guide/flows/using-conditional-steps) 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`](#flow-uses-a-capability-that-cannot-run) 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](/user-guide/records/defining-a-schema), 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:

```text
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](/user-guide/records/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:

```text
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](/user-guide/flows/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](#transform-data-language-does-not-match-the-sandbox-provider) 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:

```text
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](#approval-gate-is-enforced-on-attended-surfaces-only) 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`](#execute-agent-step-uses-a-template-variable-for-the-agent). 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](/user-guide/flows/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:

| Setting                                                              | Runs on                                                                                                                             | Refused on                                                                 |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `config.memory.enabled`                                              | Dispatch (`POST /v1/dispatch`, `POST /v1/agents/:id/execute`), product surfaces (chat, AG-UI, Product API), messaging, and webhooks | Batch, evals, schedules, MCP, and A2A                                      |
| `config.tenancyStrategy`                                             | Dispatch and product surfaces, when the request asserts a tenancy                                                                   | Batch, evals, schedules, MCP, A2A, and any request that asserts no tenancy |
| `config.sandbox.enabled`                                             | Dispatch, product surfaces, batch, evals, schedules, messaging, webhooks, Product MCP, and subagent runs                            | Voice 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:

```text
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:

```text
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:

```text
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:

```text
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](/user-guide/flows/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:

```text
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:

```text
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:

```text
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](/user-guide/flows/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:

```text
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](/user-guide/settings/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:

```text
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](#approval-gated-agent-bound-to-a-surface-or-mode-that-cannot-answer) 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](/user-guide/agents/creating-and-configuring-agents) 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](/user-guide/products-surfaces/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:

```text
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](/user-guide/products-surfaces/approvals-on-messaging-surfaces).

## Next steps

Continue with these related topics:

* [Debugging flows](/user-guide/flows/debugging-flows): trace runtime errors after you dispatch a Flow.
* [Flow variables and templates](/user-guide/flows/flow-variables-and-templates): fix variable reference warnings and pass data between steps.
* [Using prompt steps](/user-guide/flows/using-prompt-steps): set the model, reasoning, and tool options a prompt step runs with.
* [Using transform-data steps](/user-guide/flows/using-transform-data-steps): choose an execution environment and network access for a script.
* [Using record steps (get/list/upsert)](/user-guide/flows/using-record-steps-upsert-retrieve): review record step return shapes and source variables.
* [Defining a schema](/user-guide/records/defining-a-schema): declare the collection fields that record steps write.
* [Timeouts and long-running work](/user-guide/flows/timeouts-and-long-running-work): size durable pauses to fit their limits.
* [Agent versioning and publishing](/user-guide/agents/agent-versioning-and-publishing): scope Agent memory to release pointers.
* [Approvals on messaging surfaces](/user-guide/products-surfaces/approvals-on-messaging-surfaces): let people answer approval requests from Slack, Telegram, SMS, or iMessage.