Skip to main content

Dependencies and ordering

needs

A stage declares what must finish before it starts:

pipeline.ts
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.

note

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.

Only ids are namespaced

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.

SituationRun statusDependents
Stage failsfailedskipped (and so is everything else)
Stage fails, continueOnError: truesuccessrun normally
Stage skipped by whenunaffectedrun normally
Run cancelled (Ctrl-C)cancelled, exit 130skipped

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.

warning

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

OutcomeExit code
Success0
A stage failedthe failing command's own exit code
Cancelled130
Invalid pipeline, load error1

Full detail on the CLI overview.