Skip to main content

Local runs vs server runs

The execution engine is identical — flow run and a self-hosted worker call the same runPlan with the same plan. What differs is where the plan came from and what was checked before it ran.

There are three answers to that, and the difference between them is the difference between running your own code and running someone else's.

Three paths

Who builds the planExecutes your sourceChecked before running
Local flow runYour machineYesValidation only — no policy
Project runThe run host, from a fresh cloneYesValidation → policy → routing policy
Uploaded or replayedAlready built; arrives as JSONNoValidation → policy

Local

You run flow run, your machine loads your pipeline.ts, and the plan executes. There is no gate because there is no boundary — it's your code on your machine, and a policy you could edit protects nothing.

Worth knowing explicitly: a local run is never policy-checked. If you're relying on policy to stop something, it only applies on the server.

Project run

The server has a project pointing at a repository. When it triggers, the run host — the worker, in the usual deployment — clones the repo, runs the project's setup, and builds the plan with the same loader the CLI uses.

That means the run host executes the pipeline author's TypeScript, at plan time, before any policy has been evaluated. It's the same trust position as any CI system that runs a config file from a branch: whoever can change pipeline.ts can run code on the worker. Policy gates what the resulting plan may do, not what the plan-building code may do.

Once the plan exists it goes through validation, then evaluatePolicy, then routing policy — including the worker-trust gate for production labels.

Uploaded or replayed

A plan can also arrive already built, as JSON. The server validates it and evaluates policy against it without executing anything at all.

Replay is the same path: the server stored the exact IR it ran, so replaying re-reads that plan and re-checks it against current policy. A replay is not a blind re-execution — if policy has tightened since, the replay is refused.

Project runs have no stored plan of their own, which is why they are retried, not replayed: a retry re-clones and rebuilds, and can therefore pick up a different commit.

Why policy is shaped like the plan

Policy rules are written against the execution plan — stage ids, commands, container images — rather than against your TypeScript. From the source:

Because it operates on the serializable IR — not closures — a server can gate uploaded plans before running them.

That's the whole argument. A policy check against TypeScript would have to execute the pipeline definition to inspect it, which is exactly what you can't do with an untrusted plan. Because the IR is plain JSON, a server can read every command a plan will run, decide, and refuse — having executed nothing.

It also means policy is checkable in places source doesn't exist: an uploaded plan, a replayed one, a plan in a database.

The rules themselves are on Worker — one page, so there's one place to look when a run is refused.

What else differs

Everything below is a matter of what the caller wires up, not of the engine behaving differently.

LocalServer / worker
CredentialsNo resolver — declare them for portability, supply them through your environmentResolved just-in-time by the worker, from the configured providers
ArtifactsNot collectedStored via the filesystem or S3
HistoryLocal SQLite, --no-history to skipAlways recorded, plus a durable event log a client can reconnect to
Cache.flowwright/cache in the working directoryA shared root every worker uses
SchedulingOne run, foregroundA shared queue with leases, so a crashed worker's run is requeued

Stash behaves the same in both — it's run-scoped and ephemeral either way.

Which to use

Run locally while you're writing the pipeline; that's the point of the CLI, and it's the fastest loop. Put flow run in your existing CI when you want it on every push without operating anything. Run the server when you want central triggering, history that outlives a job, a tenancy boundary around each team, people who log in — and policy that the person writing the pipeline can't turn off.