Matrix stages
matrix() generates one stage per combination of the values you give it.
import { pipeline, stage, sh, matrix } from "@flowwright/core";
export default pipeline({
name: "acme-lib",
concurrency: 4,
stages: [
stage("Install", async () => {
await sh(["npm", "ci"]);
}),
...matrix(
"Test",
{ node: ["20", "22", "24"] },
{
needs: ["install"],
run: async ({ node }) => {
await sh(["npm", "test"], { env: { NODE_VERSION: node } });
},
},
),
],
});
Three stages, one per Node version. The combination is the first argument to run,
and it's typed from the dimensions — node is "20" | "22" | "24", so a typo is a
compile error.
Names and ids
The combination is appended to the name:
| Combination | Name | Id |
|---|---|---|
{ node: "20" } | Test (node=20) | test-node-20 |
{ node: "20", os: "linux" } | Test (node=20, os=linux) | test-node-20-os-linux |
Those ids are what needs elsewhere refers to. Dimensions are combined in
declaration order, with the first key varying slowest.
For a Node version matrix specifically, plugin-node's
nodeVersions builds the same thing with the containers already wired.
More than one dimension
...matrix(
"E2E",
{ browser: ["chrome", "firefox"], shard: ["1", "2"] },
{
needs: ["build"],
container: "mcr.microsoft.com/playwright:v1.49.0",
run: async ({ browser, shard }) => {
await sh(["npx", "playwright", "test", "--project", browser, "--shard", `${shard}/2`]);
},
},
)
Four stages. They all share the same needs and never depend on each other, so with
concurrency above 1 they fan out in parallel.
An empty dimension array produces zero stages — worth knowing if the list is computed.
What options a matrix accepts
matrix() takes a subset of the normal stage options:
| Option | Accepted |
|---|---|
needs | Yes |
container | Yes |
when | Yes |
timeout | Yes |
continueOnError | Yes |
retries | Yes |
services | No |
credentials | No |
produces | No |
id | No |
The four missing ones aren't silently dropped — they're not on the type, so TypeScript rejects them.
When you need them, build the stages with a plain .map() instead. matrix() is a
convenience over exactly that:
...["20", "22", "24"].map((node) =>
stage(`Test (node=${node})`, {
needs: ["install"],
container: `node:${node}`,
credentials: ["npm-token"],
produces: [`reports/${node}.xml`],
run: async () => {
await sh(["npm", "test"]);
},
}),
)
You lose the generated naming and the typed combination, and get the full option set back.
Checking what you generated
flow explain lists every generated stage — the fastest way to confirm the grid is
what you meant:
test-node-20 ← install
$ npm test
test-node-22 ← install
$ npm test
test-node-24 ← install
$ npm test