Skip to main content

Inspecting a pipeline

Three commands that read without executing. All of them are safe to run anywhere.

flow explain

Prints the execution plan — the stages in dependency order, what each one will run, and nothing else happens.

flow explain
Pipeline: acme-api

install
$ npm ci
lint ← install
$ npm run lint
typecheck ← install
$ npm run typecheck
test ← install
$ npm run test
build ← lint, typecheck, test
$ npm run build

Stages are listed in topological order. Annotations appear only when they apply:

NotationMeaning
← a, bDepends on stages a and b
$ …A command this stage will run
[image]Runs in a container
when: branch=mainConditional on branch
when: tag~v*Conditional on tag
when: dynamicGuarded by a function — can't be resolved ahead of time
⧉ services: …Sidecar containers
→ produces: …Declared artifacts
⇄ …Stash operations
↺ …Cache restore/save
⚠ continueOnErrorA failure here won't fail the run

A stage with no $ lines isn't a mistake — it's usually a join barrier created by parallel(…, { join: "checks" }), which exists to be depended on rather than to run anything.

If the pipeline sets concurrency above 1, explain says so under the name, which is the quickest way to see the default flow run will actually use.

Machine-readable

flow explain --json
{
"version": 2,
"name": "acme-api",
"stages": [
{
"id": "install",
"name": "Install",
"needs": [],
"when": { "kind": "always" },
"timeout": null,
"continueOnError": false,
"retries": 0,
"steps": [
{
"id": "install:0",
"shell": false,
"cmd": ["npm", "ci"],
"raw": null,
"cwd": ".",
"env": {},
"timeout": null
}
],
"produces": [],
"cacheKeys": [],
"dynamic": false
}
]
}

This is the execution plan itself — the same structure the runtime executes and workers receive, not a reporting format invented for the flag. Every field is on the Execution plan reference.

The human view above is a summary — it shows the graph. --json is the complete plan: retries, timeout, step env and cwd, and the credential list appear only there.

note

explain validates before printing, in both forms. An invalid pipeline prints the same errors as flow validate and exits 1 rather than describing an order that could never run.

flow validate

Loads the pipeline and checks it, without running anything.

flow validate
✓ pipeline.ts is valid (5 stages)

When something's wrong, you get the location, the problem, and a code:

✖ invalid pipeline (1 error)
• stages[0].needs[0]: needs references unknown stage "nope" [unknown_need]

Exit code is 1 on failure, which makes flow validate a reasonable pre-commit hook. It catches unknown needs, dependency cycles, duplicate stage IDs, malformed credentials and similar structural problems — everything checkable without running a command.

The bracketed code is the thing to search for. All 27 of them, with what triggers each and how to fix it, are on Validation errors.

flow doctor

Checks the environment rather than the pipeline.

flow doctor
FlowWright doctor

✓ Node 24.15.0 — node:sqlite available (run history works)
✓ pipeline.ts loads and validates (5 stages)
✓ Docker available — container stages run isolated
⚠ not a git repo — branch/commit context won't be recorded
✓ .flowwright is writable (run history + cache)

✓ ready
CheckFails the run?If it's not happy
Node + node:sqliteNoUpgrade to Node 24+. Below that, history commands re-exec with --experimental-sqlite.
Pipeline fileYesA missing file is a warning — run flow init. A file that fails to load or validate is a failure; run flow validate.
DockerNoContainer stages fall back to the host. Fine if you don't use them.
Git contextNoBranch and commit won't be recorded against runs.
.flowwright writableYesCheck directory permissions.

Warnings () don't affect the exit code — only the two failure cases do. doctor exits 0 when nothing failed and 1 when something did.

doctor doesn't create anything. It checks that it could write, then cleans up after itself, so running it in a fresh checkout won't leave a .flowwright/ behind.