
Engineering
State machines vs. flow DAGs: two ways to model a workflow
FastYoke Engineering · 8 min read · Aug 22, 2026
- Architecture
- FSM
- Workflow
The problem: two diagrams that look identical
Open a flow-automation tool and an operational platform side by side and you'll see the same picture: boxes connected by arrows. It's tempting to conclude they're the same idea with different branding. They aren't. A flow DAG and a finite state machine draw similar diagrams because both are directed graphs — but they are graphs of different things, and picking the wrong one is the kind of mistake you don't notice until six months in, when a question you should be able to answer instantly turns into an afternoon of reading logs.
Here's the question that separates them: what does a node mean? In a flow
DAG, a node is a step of work — run this script, call this API, transform this
payload. In a finite state machine, a node is a state a record can be in —
awaiting_review, dispatched, closed. The arrows differ to match. DAG edges
mean "then do this next." FSM edges mean "this record is allowed to move from
here to there, if the guard passes." One models a procedure. The other models
a lifecycle.
DAGs describe a procedure; FSMs describe a lifecycle
Windmill is a good, honest example of the DAG-first model, so let's use it. A Windmill flow is a directed acyclic graph of steps — scripts in Python, TypeScript, Go, Bash, SQL — wired together with branches, retries, and schedules. It's a genuinely excellent tool for its target: you have code that needs to run in a particular order, on a trigger or a cron, with failure handling, across languages. ETL, glue jobs, scheduled automations, internal back-office scripts. If your problem is "run these things in this order, reliably," a flow DAG is exactly the right abstraction, and Windmill executes it well.
But notice what a DAG is silent about. It describes how work flows, not what states an entity is allowed to occupy. If your domain object is an order, a support case, a shipment, a loan application — something with a life of its own that many different actors nudge through many different paths over days or weeks — the DAG doesn't natively answer "what state is this order in, and who is allowed to move it out of that state?" You can encode that inside step logic, but now the rules of the lifecycle live scattered across imperative scripts rather than in a model you can point at.
A finite state machine inverts the priority. The primary artifact is the set of legal states and the guarded transitions between them. The procedure — the actual work done on each move — hangs off the transitions as actions. The DAG asks "what runs next?" The FSM asks "where is this allowed to go from here, and who's allowed to take it there?"
What each model makes easy — and what it hides
The reason this matters isn't aesthetic. The shape you choose determines which questions are cheap to answer and which are expensive.
A flow DAG makes execution legible. You can see a run: which steps fired,
what they returned, where it failed, how the retry went. That history is about
runs — individual executions of the procedure. What it doesn't give you for
free is a verifiable model of an entity's allowed lifecycle. "Could an order ever
reach refunded without passing through paid?" is not a question you ask a pile
of run logs; it's a question you ask a state graph. In a DAG world you answer it
by reading every script that might touch the order's status field and reasoning
about all their branches. In an FSM world you answer it by looking at the graph:
if there's no edge into refunded except from paid, the answer is no, by
construction.
A finite state machine makes the lifecycle legible and, crucially, checkable. Because the legal moves are enumerated rather than implied, you get properties a DAG can't offer:
- Reachability and dead-state analysis. You can statically ask "can a record ever get here?" and "is there a state it can get stuck in?" — before shipping.
- Illegal moves rejected by construction. A transition that isn't in the schema simply can't fire. There's no code path to forget to guard, because the guard is the model.
- An audit trail that is the model's own byproduct. Every legal move appends to an immutable event log — who moved it, when, from what to what. We wrote up why that ledger is append-only, and how out-of-band overrides stay auditable, in Append-only truth.
What the FSM hides, in turn, is procedural nuance. If your real problem is a ten-step data pipeline where "state" is a meaningless concept and the whole point is the sequence of transformations, forcing it into a state machine is ceremony — you'd be inventing states to describe what is really just step 1, step 2, step 3. That's the DAG's job. Use it.
How FastYoke uses both
FastYoke picks the finite state machine as the spine — because the domains it's built for (operations: jobs, cases, orders, shipments, tickets) are lifecycles, not pipelines. Named states, guarded transitions, self-loops for audit-only events, and an out-of-band admin override for the times a human has to force a record somewhere the graph wouldn't allow. Every firing lands in the append-only event log. That's the right abstraction for operational software specifically because the questions operators actually ask — who moved this, could it have gotten here, what's it allowed to do next — are the questions a state graph answers cheaply.
But "FSM as the spine" doesn't mean throwing away imperative work. Real transitions do things: call a payment API, render a PDF, re-score a route, notify a downstream system. FastYoke runs that imperative logic as guarded actions on a transition — and heavier custom logic runs in a sandboxed scripting tier (a WebAssembly sandbox with resource limits and no host syscalls, the model we described in three ways to run untrusted code). So you get the procedure inside the lifecycle: the state machine decides what moves are legal and records them; the scripts do the work each move entails. It's not FSM instead of imperative steps — it's imperative steps bounded by a verifiable state model.
That's the practical answer to "FSM or DAG": for an entity with a lifecycle, make the lifecycle the model and let the procedures hang off its transitions. You keep the reachability guarantees, the by-construction rejection of illegal moves, and the audit ledger — and you still run whatever code each step needs.
What to watch out for
The failure mode in both directions is forcing the wrong shape.
Forcing a lifecycle into a DAG looks fine at first — you wire up a flow that
moves an order along — and then the lifecycle logic metastasizes. The rule "only
a manager can move a flagged order to approved" ends up as an if inside step
four of one flow and step two of another, and now the authoritative definition of
your order's lifecycle is "read all the flows and hope you found every branch."
The audit answer is "grep the run history." This is the tax you pay for using a
procedure model to describe a lifecycle.
Forcing a pipeline into an FSM is the opposite error and just as real. If your work is genuinely a sequence of transformations with no meaningful notion of "what state is this allowed to be in," inventing states to satisfy a state machine adds ceremony without buying you the guarantees — because there were no illegal moves to reject in the first place. That's when a flow tool like Windmill is the honest choice, and reaching for an FSM is over-engineering.
The tell is the audit question. If your users will one day ask "who moved this, and could it legally have gotten here?" — model the lifecycle. If they'll only ever ask "did last night's job run?" — model the procedure.
Where this goes next
If your product is an operational platform — orders, cases, jobs, anything with a status that many people move through many paths — the state machine isn't a stylistic preference, it's what makes the hard questions cheap. Start with FSM: the right abstraction for operational software for the core argument, and Append-only truth for how the event ledger underneath it stays honest. If you're weighing whether to build any of this at all, build vs. buy vs. configure is the companion piece.
And if your problem really is "run these scripts in this order" — use a flow tool, and don't let anyone talk you into a state machine you don't need.