Flow validation warnings

Runtype validates a Flow every time you save it, and every time you validate or create one through the API. Some checks catch configuration mistakes that pass schema validation but fail at runtime. These surface as non-blocking warnings so you can fix them before you dispatch, not after a run fails.

How validation warnings work

Validation runs at save time in the dashboard, when you call the validate endpoint (POST /v1/public/flows/validate) or the validate_flow MCP tool, and when you create or update a Flow. The response is a single envelope:

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

Warnings do not block a save (valid stays true when there are no errors), but each one predicts a failure that would otherwise only show up at dispatch time. Read warnings[].code to identify the problem and warnings[].message for the exact fix. The path and step fields point to the step that triggered it.

The rest of this page catalogs each code, when it fires, and how to fix it. Most land in warnings[]. Codes that are pure optimization hints (they never predict a failure) land in recommendations[] instead, and are noted as such below.

List Records output read with object access

Code: LIST_RECORDS_OBJECT_ACCESS

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

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

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

A get-record step always returns a single object, so object-style access is correct there and this warning does not fire. A deprecated retrieve-record step is checked under whichever of the two it resolves to (see Migrating from retrieve-record). See Using record steps (get/list/upsert) for the full return-shape rules.

Deprecated step type: retrieve-record

Code: DEPRECATED_STEP_TYPE

retrieve-record is deprecated in favor of Get Record and List Records. The step still works exactly as before: Runtype resolves an ID-based lookup as get-record and a type/name/filter lookup as list-records at execution time. This warning is a non-blocking reminder to migrate the step the next time you edit the Flow.

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

Upsert-record source variable is not a JSON object

Code: UPSERT_RECORD_SOURCE_NOT_JSON

An upsert-record step’s sourceVariable must reference a JSON object, because the record metadata column is JSONB. The most common mistake is feeding a prompt step’s plain-text output (any responseFormat other than json) straight into upsert-record without a contentField. That produces a string, which fails at runtime.

The validation message names the offending prompt step and gives you three ways to fix it:

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

Store-vector source variable is unresolved

Code: STORE_VECTOR_SOURCE_UNRESOLVED

A store-vector step resolves its vectorsSource by variable lookup. If the named variable is not produced by any earlier step and is not a declared flow input, the step throws Could not resolve vectors at dispatch time.

The validation message names the missing variable and the usual source for it:

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 with "Could not resolve vectors". Set a prior step's outputVariable to "embeddings" (typically a generate-embedding step), or declare "embeddings" as a flow input.

Tool call strategy “required” on a multi-step prompt

Code: TOOL_STRATEGY_REQUIRED_MULTISTEP

Setting toolCallStrategy: "required" on a prompt step forces the model to call a tool on every step. Unless the turn is capped to a single tool call, the model can never return a final text answer, so the output comes back empty. This fires when maxToolCalls is unset (the runtime default allows several calls), set above 1, or the prompt runs in a multi-turn loop.

The validation message spells out the safe 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).

See Using prompt steps for prompt-step tool configuration.

Conditional compares an unquoted template against a string

Code: CONDITION_UNQUOTED_TEMPLATE_COMPARISON

A condition or when predicate is JavaScript that runs after template substitution. If you compare an unquoted {{...}} placeholder against a string literal ({{analysis.health}} === 'watch'), a non-numeric value substitutes in as a bare identifier (healthy === 'watch') and throws a ReferenceError at runtime.

The validation message shows the offending snippet 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.

Quote the placeholder ('{{analysis.health}}' === 'watch') for string comparisons, or compare numerically when the value is a number. See Using conditional steps for more on condition expressions.

System prompt embeds a per-run variable

Code: CACHE_VOLATILE_SYSTEM_PROMPT (appears in recommendations[])

A prompt step’s system prompt embeds a per-run temporal variable ({{_now}}, {{_execution.*}}, or {{_schedule.*}}). Because that value changes on every execution, the system prompt is different each run, so Runtype’s automatic prompt caching cannot reuse the cached prefix across requests. This is a non-blocking optimization hint, not an error. The flow runs exactly the same. It just caches worse than it could.

The recommendation message names the offending 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 byte-identical across runs. See Prompt caching for how automatic caching works.

Record step writes a field the collection schema does not declare

Code: RECORD_FIELD_NOT_IN_SCHEMA

When an upsert-record or update-record step targets a record type that is a schematized collection, Runtype checks the fields the step writes against the schema. If the step writes a field the schema does not declare and the schema closes additionalProperties (sets it to false), the write is badged in warn mode and rejected in enforce mode at runtime. This warning surfaces that at save time.

Runtype can only check fields it can read statically: an update-record step’s updates map keys, and an upsert-record step’s contentField. It fires regardless of the collection’s validationMode, because the schema is the collection’s declared data model either way. Collections whose schema leaves additionalProperties open accept extra fields, so this warning does not fire for them.

The validation message names the field, the collection, and the 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 (see Defining a schema), or remove it from the step’s updates / contentField.

Record step leaves a required schema field unmapped

Code: RECORD_REQUIRED_FIELD_UNMAPPED (appears in recommendations[])

When an update-record step uses mergeStrategy: "replace" (which overwrites the record’s whole metadata, so the fields it writes are the complete record), Runtype checks that every field the target collection’s schema marks required is set. Any required field the step never maps produces this recommendation, so you notice the gap before the replaced record fails schema validation.

This is a recommendation, not a warning: it lands in recommendations[], and it fires only for complete (replace) mappings. A merge or deep-merge write can inherit the required field from the existing record, so it never triggers this. Like the field warning above, it is independent of the collection’s validationMode.

The recommendation message names the required field, the collection, and the 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 drop it from the schema’s required list (see Defining a schema).

Next steps