Dependencies and ordering
needs
A stage declares what must finish before it starts:
import { pipeline, stage, sh } from "@flowwright/core";
export default pipeline({
name: "acme-api",
concurrency: 3,
stages: [
stage("Install", async () => {
await sh(["npm", "ci"]);
}),
stage("Lint", { needs: ["install"], run: async () => void (await sh(["npm", "run", "lint"])) }),
stage("Test", { needs: ["install"], run: async () => void (await sh(["npm", "test"])) }),
stage("Build", {
needs: ["lint", "test"],
run: async () => void (await sh(["npm", "run", "build"])),
}),
],
});
needs takes stage ids, not names — see Stages.
Order without needs
By default a pipeline runs strictly sequentially, in array order. A stage with no
needs still waits for the one before it.
Raise concurrency and that changes: independent stages overlap, and array order
stops meaning anything.
export default pipeline({
name: "acme-api",
concurrency: 4,
stages: [/* … */],
});
flow run -j <n> overrides whatever the pipeline declares. See
Running pipelines.
Because array order is a real guarantee at concurrency: 1, a pipeline can work by
accident and break the day you raise it. flow explain shows the ← edges — if two
stages you expect to be ordered have no edge between them, add the needs.
parallel()
For a fan-out that shares dependencies:
import { parallel } from "@flowwright/core";
...parallel(
[
stage("Lint", async () => void (await sh(["npm", "run", "lint"]))),
stage("Test", async () => void (await sh(["npm", "test"]))),
stage("Typecheck", async () => void (await sh(["npm", "run", "typecheck"]))),
],
{ needs: ["install"], join: "checks" },
)
needs is applied to every member. join adds a barrier stage — an empty stage that
depends on all of them — so downstream work can depend on the whole group by one id:
stage("Build", { needs: ["checks"], run: async () => {} });
The barrier shows up in flow explain as a stage with no commands, which is
expected:
checks ← lint, test, typecheck
Members must be independent. If one needs another, parallel() throws while the
plan is being built:
parallel members must be independent: "web" needs sibling "api"
group()
Namespaces a set of stages, which matters when a shared helper is used twice:
import { group } from "@flowwright/core";
export default pipeline({
name: "monorepo",
stages: [...group("api", buildStages()), ...group("web", buildStages())],
});
group("api", …) rewrites each member's id to api-<id>, and rewrites needs only
where they point at another member of the same group. Edges leaving the group are
left alone, so a group can still depend on something outside it.
produces paths and cache keys are not prefixed. Two groups built from the same
helper will share cache keys and declare the same artifact paths unless you vary them
yourself — usually by working the group name into the key.
How failure spreads
This is the part worth reading carefully, because it's stricter than most CI systems.
One hard failure skips everything that hasn't started — not just the failed
stage's dependents. An unrelated stage later in the pipeline is skipped too, with
reason upstream-failed. FlowWright fail-fasts the whole run rather than pruning a
subtree. Stages already running when the failure lands are allowed to finish.
| Situation | Run status | Dependents |
|---|---|---|
| Stage fails | failed | skipped (and so is everything else) |
Stage fails, continueOnError: true | success | run normally |
Stage skipped by when | unaffected | run normally |
| Run cancelled (Ctrl-C) | cancelled, exit 130 | skipped |
continueOnError
stage("Upload coverage", {
needs: ["test"],
continueOnError: true,
run: async () => void (await sh(["./upload-coverage.sh"])),
});
The stage still shows ✗ and is recorded as failed, but the run stays green and
keeps going.
continueOnError doesn't just keep the run green — dependents run anyway, on
whatever the failed stage left behind. If a failing build is
continueOnError and deploy needs it, deploy ships a broken or missing artifact.
Use it for stages whose output nothing depends on: notifications, coverage uploads,
best-effort cleanup.
A stage skipped by when behaves differently — it
doesn't block its dependents, because "this branch doesn't need it" isn't a failure.
Exit codes
| Outcome | Exit code |
|---|---|
| Success | 0 |
| A stage failed | the failing command's own exit code |
| Cancelled | 130 |
| Invalid pipeline, load error | 1 |
Full detail on the CLI overview.