Skip to main content

Validation errors

flow validate loads your pipeline, builds the execution plan, and checks it against the IR contract. Failures look like this:

$ flow validate
✖ invalid pipeline (4 errors)
• stages[1].timeout: duration "10" has no unit — did you mean "10s"? (a bare number is milliseconds) [bad_duration]
• stages[1].retries: retries must be >= 0 [negative_retries]
• stages[0].needs[0]: needs references unknown stage "nope" [unknown_need]
• stages[1].needs[0]: stage "test" cannot depend on itself [self_need]

Each line is path: message [code]. The path locates the field in the plan JSON, not in your TypeScript — stages[1] is the second stage in the built plan. flow run, flow explain and flow doctor all run the same validation and refuse the same plans.

There is no warning severity. Every issue is fatal and exits 1. A plan is valid or it isn't.

Two kinds of code

Some codes you can trigger from pipeline.ts. Others exist for plans that arrive as JSON — submitted to a server, stored and replayed, or generated by another tool — where the shape itself is untrusted. The TypeScript API constructs the plan for you, so it can't produce a missing field or a wrong type.

The structural codes — not_object, unknown_field, missing_field, wrong_type, bad_version, unsupported_version — are almost always in that second group. If you're seeing one from flow validate, you have a hand-edited plan or a version mismatch, not a pipeline bug.

Version and shape

not_object

Something that must be an object isn't. The message names what: plan, stage, post handler, stash, service, step, cacheKey or credential.

Almost always a hand-written or machine-generated plan with a null, an array, or a string where an object belongs.

unknown_field

unknown field "concurrancy"

An object carries a key the contract doesn't define. The validator allow-lists keys per object type rather than ignoring extras, so a typo fails loudly instead of being silently dropped. Check the spelling against the field tables.

missing_field

A required field is absent — version is required, name is required, stages is required, or a required id or boolean on a nested object. Distinguished from wrong_type, which is what you get when the field is present but wrong.

wrong_type

The single most common structural code. The message always names the expected type: name must be a string, stages must be an array, needs must be an array of strings, retries must be an integer, env must be a string record, container must be a non-empty string.

bad_version

version must be a positive integer

version is present but isn't a positive integer — a string "2" rather than 2 is the usual cause.

unsupported_version

plan version 3 is newer than supported (2) — produced by a newer FlowWright

The plan was produced by a newer FlowWright than the one reading it. Upgrade the reader. Only newer is rejected; an older plan is fine as long as its shape still validates.

Plan level

bad_concurrency

concurrency must be a positive integer

concurrency is present but isn't a positive integer. Omit it entirely for sequential execution rather than passing 0.

empty_name

name must not be empty

A required string is "". Applies to the pipeline name, a stage name, a service image, a cache path or key, a stash name, and a credential id or type.

bad_post

a post handler is out of the DAG — its needs must be empty

A post.always/success/failure handler declared needs. Post handlers run after the whole DAG completes, on the outcome — they aren't nodes in the graph and can't depend on one. Drop the needs.

Stages and the graph

bad_stage_id

stage id "Build Step" must match /^[a-z0-9][a-z0-9-]*$/

Stage ids are lowercase alphanumerics and hyphens, starting with a letter or digit. Ids are derived from the stage name by slugging it, so this usually means an explicit id: was set by hand. They're constrained because they appear in step ids, log paths, CI section names and URLs.

duplicate_stage_id

duplicate stage id "test"

Two stages share an id. Rare from pipeline.ts — the builder de-duplicates by suffixing, so a second stage("test") becomes test-2. Worth knowing: if you meant to have one stage and got two, your needs: ["test"] points at the first.

unknown_need

needs references unknown stage "nope"

A needs entry names a stage that isn't in the plan. Usually a typo, or the id you wrote differs from the slug of the stage's name.

self_need

stage "test" cannot depend on itself

A stage lists its own id in needs.

cycle

dependency cycle: build -> test -> build

The needs graph isn't acyclic. The message traces the cycle, so read it as a path and break one edge.

negative_retries

retries must be >= 0

retries is an integer below zero. 0 means "run once, don't retry".

bad_duration

Two messages, one code:

duration "10" has no unit — did you mean "10s"? (a bare number is milliseconds)
invalid duration "10 sec" — use 500ms, 30s, 10m or 2h

The first is the trap worth naming: a unitless duration parses as milliseconds, so timeout: "10" would be a 10ms timeout. It's rejected rather than silently honoured. Valid units are ms, s, m, h.

Steps

duplicate_step_id

duplicate step id "build:0" within stage

Step ids are unique within their stage and normally generated as "<stageId>:<index>". Only reachable with hand-built steps.

step_inconsistent

The shell/cmd/raw/write combination doesn't form a valid step. Messages:

MessageMeans
shell:false requires cmd: string[]An exec step needs its argv array
shell:false requires raw: nullAn exec step can't carry a shell string
an exec step must not carry writePick one of exec or write
shell:true requires raw: stringA shell step needs its command string
shell:true requires cmd: nullA shell step can't carry argv
a shell step must not carry writePick one of shell or write
a write step requires write: { path, content }The write payload is missing or malformed
a write step requires raw: nullA write step can't carry a shell string

The three valid shapes are in the step table.

Conditions, cache and stash

bad_when

Covers every malformed when:

when must be an object with a string kind
unknown when kind "on-branch"
branch when requires equals: string
tag when requires matches: string
dynamic when source must be a string
unexpected field "matches" for when kind

The last one is the interesting case: the field is valid for some kind, just not this one. { kind: "branch", matches: "v*" } fails because branch takes equals and tag takes matches.

bad_cache_op

op must be "restore" or "save"

bad_stash_op

op must be "stash" or "unstash"

Note the asymmetry: cache uses restore/save, stash uses stash/unstash. They're different words for opposite-looking pairs, and mixing them is the usual cause.

Services

bad_service

service name must match /^[a-z0-9][a-z0-9-]*$/ (it is the service's hostname)

The constraint exists because the name is the DNS name your steps reach the service at. http://postgres works; http://My_DB doesn't.

duplicate_service_name

duplicate service name "postgres"

Two services in one stage share a name — so two containers would claim one hostname.

Credentials

bad_credentials

Three messages:

credential binding must be an object with a string kind
unknown credential binding kind "token"
binding env var must match /^[A-Za-z_][A-Za-z0-9_]*$/

The valid kinds are env, file, usernamePassword and sshKey. The env-var rule is the shell's own identifier rule — no hyphens, no leading digit. See Credentials.

duplicate_credential_id

duplicate credential id "npm-token" within stage

One stage declares the same credential twice. Declare it once; the binding injects it for the whole stage.

duplicate_credential_env

duplicate credential env binding "NPM_TOKEN" within stage

Two credentials in one stage bind to the same environment variable, so one would silently overwrite the other. All four binding kinds share one namespace — a usernamePassword whose userEnv collides with another credential's env name is caught here too.

unsupported_credentials

plan requires credentials but this runtime does not support them

A stage declares credentials and the runtime reading the plan can't resolve or inject them, so it refuses rather than running the stage with the secret missing.

You will not see this from the CLI. The CLI has no credential resolver by design — locally you supply secrets through your own environment, and a stage with credentials: runs anyway. This code exists for runtimes that embed validateExecutionPlan and explicitly declare they can't handle credentials. If you're trying to reproduce it from flow validate, you can't.