Skip to main content

Conditional stages

Some stages shouldn't run every time. when decides, at run time, whether a stage executes.

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