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:
| Message | Means |
|---|---|
shell:false requires cmd: string[] | An exec step needs its argv array |
shell:false requires raw: null | An exec step can't carry a shell string |
an exec step must not carry write | Pick one of exec or write |
shell:true requires raw: string | A shell step needs its command string |
shell:true requires cmd: null | A shell step can't carry argv |
a shell step must not carry write | Pick one of shell or write |
a write step requires write: { path, content } | The write payload is missing or malformed |
a write step requires raw: null | A 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.