Skip to main content

Quickstart

FlowWright runs your pipeline on your machine. No account, no server, no YAML — a pipeline.ts file and one command.

Requirements

Node.js 24 or newer. That's the whole list. Docker is optional and only needed if a stage declares a container.

Install

Run it without installing anything:

npx @flowwright/cli init

Or add it to the project, which is what most teams do — it pins the version in your lockfile alongside everything else:

npm install --save-dev @flowwright/cli

Either way the binary is called flow.

Scaffold a pipeline

From the root of an existing project:

flow init
✓ created pipeline.ts
detected: npm · lint, typecheck, test, build

next: flow run

flow init reads your project before writing anything. It picks the package manager from your lockfile and looks for the scripts it knows how to wire up — lint, typecheck, test, build — so the pipeline it writes matches the project you already have.

For a project with all four scripts and a package-lock.json, that's:

import { pipeline, stage, sh } from "@flowwright/core";

export default pipeline({
name: "acme-api",
stages: [
stage("Install", async () => {
await sh(["npm", "ci"]);
}),
stage("Lint", {
needs: ["install"],
run: async () => {
await sh(["npm", "run", "lint"]);
},
}),
stage("Typecheck", {
needs: ["install"],
run: async () => {
await sh(["npm", "run", "typecheck"]);
},
}),
stage("Test", {
needs: ["install"],
run: async () => {
await sh(["npm", "run", "test"]);
},
}),
stage("Build", {
needs: ["lint", "typecheck", "test"],
run: async () => {
await sh(["npm", "run", "build"]);
},
}),
],
});

Reading it top to bottom:

  • pipeline({ name, stages }) is the whole file. It's a default export, so the CLI can load it.
  • stage("Install", async () => { … }) is the short form: a name and a body.
  • stage("Lint", { needs: ["install"], run }) is the options form. needs is what builds the graph — Lint waits for Install.
  • Stage IDs are derived from names. "Typecheck" becomes typecheck, which is what needs refers to and what shows up in logs and reports.
  • sh(["npm", "run", "lint"]) takes an argv array, so there's no shell to quote against and nothing to escape.
  • Build needs all three checks, so it only starts once they've all passed.

Nothing here is FlowWright-specific configuration — it's a TypeScript file. Your editor autocompletes it and typechecks it like any other. Every option is covered in Stages.

Install the DSL package so the imports resolve:

npm install --save-dev @flowwright/core

Run it

flow run
FlowWright · acme-api

▶ Install
$ npm ci
[07:10:53] added 1 package, and audited 2 packages in 353ms
[07:10:53] found 0 vulnerabilities
✓ install 403ms

▶ Lint
$ npm run lint
[07:10:53] lint: 0 problems
✓ lint 103ms

▶ Typecheck
$ npm run typecheck
[07:10:53] typecheck: 0 errors
✓ typecheck 71ms

▶ Test
$ npm run test
[07:10:53] test: 12 passed
✓ test 67ms

▶ Build
$ npm run build
[07:10:53] build: dist/ written
✓ build 68ms

✔ Run succeeded in 717ms

Every command is echoed before it runs, output streams through live, and each stage reports its own duration. If a stage fails, the run stops and flow exits with that command's exit code — so flow run drops into a CI job or a git hook unchanged.

See the plan without running it

flow explain
Pipeline: acme-api

install
$ npm ci
lint ← install
$ npm run lint
typecheck ← install
$ npm run typecheck
test ← install
$ npm run test
build ← lint, typecheck, test
$ npm run build

The arrows are the dependency graph. explain never executes anything, which makes it the fastest way to check a change to pipeline.ts did what you meant.

Notice that Lint, Typecheck and Test all depend only on Install — they're independent of each other, so they can run at the same time:

flow run -j 3

Where the run went

flow run created a .flowwright/ directory next to your pipeline.ts:

.flowwright/
├── state.db run history — what ran, when, and whether it passed
├── runs/<run-id>/
│ ├── run.json the run's summary
│ └── logs/<stage>.log full output, one file per stage
├── cache/ content-addressed cache
└── stash/ scratch space, scoped to a run

It's local, it's yours, and it's disposable — add it to .gitignore. Every past run stays queryable with flow list and flow logs.

Next