Conditional stages
Some stages shouldn't run every time. when decides, at run time, whether a stage
executes.
import { pipeline, stage, sh, onBranch, onTag } from "@flowwright/core";
export default pipeline({
name: "acme-api",
stages: [
stage("Test", async () => {
await sh(["npm", "test"]);
}),
stage("Deploy staging", {
needs: ["test"],
when: onBranch("main"),
run: async () => {
await sh(["./deploy.sh", "staging"]);
},
}),
stage("Publish", {
needs: ["test"],
when: onTag("v*"),
run: async () => {
await sh(["npm", "publish"]);
},
}),
],
});
onBranch
when: onBranch("main");
An exact match against the current branch.
onTag
when: onTag("v*");
A glob against the tag pointing at the current commit. * matches any run of
characters; it's the only wildcard. v* matches v1.0.0, release-* matches
release-2024-05.
If the commit has no tag, the stage is skipped.
Where the branch and tag come from
FlowWright reads git at run time:
- branch — the current branch, or nothing at all in a detached HEAD
- tag — the first tag pointing at
HEAD
All of it is best-effort. Outside a git repository — or with no git binary —
there's no branch and no tag, so every onBranch and onTag stage is skipped.
flow doctor warns about this:
⚠ not a git repo — branch/commit context won't be recorded
Detached HEAD is common in CI. Many providers check out a specific commit rather than
a branch, in which case onBranch("main") never matches. If a deploy stage mysteriously
never runs in CI but works locally, that's the first thing to check.
Why not just an if?
Because a stage body runs at plan time, when there is no branch:
// This does not work.
stage("Deploy", async (ctx) => {
if (ctx.git.branch === "main") {
await sh(["./deploy.sh"]);
}
});
ctx.git is empty during recording, so the condition is false, the body records
zero steps, and the stage does nothing on every branch — including main. It
doesn't fail; it just quietly becomes a no-op. See
the overview for why.
when avoids this by being data rather than code. It's recorded into the plan and
evaluated later:
deploy-staging ← test when: branch=main
publish ← test when: tag~v*
That line in flow explain is the other reason to prefer it — you can see the
condition without running anything. A condition hidden inside a closure would be
invisible.
What skipping does to the rest of the graph
A stage skipped by when does not block its dependents. They run as normal.
That's deliberate: "this branch doesn't need a deploy" isn't a failure, and it shouldn't cascade. It differs from a failed stage, which skips everything remaining — see Dependencies.
If a dependent genuinely can't work without the skipped stage, give it the same
when.
Skipped stages appear in the run output with their reason:
⊘ deploy-staging skipped (when)