Beyond the Application State Machine: Why AI Coding Needs a Workflow Compiler
We build systems for other people, on contract. That single fact decides what I pay attention to.
Beyond the Application State Machine: Why AI Coding Needs a Workflow Compiler

We build systems for other people, on contract. That single fact decides what I pay attention to.
For a while now — really, for most of this year — the question I can’t put down is how far AI-driven development can actually be pushed. Not in a demo. In the work we ship, with real constraints, real deadlines, and someone else’s production system on the other end.
Most of that question turns out to be about the repository itself: can an agent operate inside it? I’ve written before about treating AI readability as a first-class design constraint, and this year I’ve spent most of my time testing what actually moves it — which tools, which conventions, which guardrails earn their place in a codebase an agent has to work in.
We take the good tools where they exist. But at some point this year the gap got concrete enough that we started building our own: a large private SDK, now in real use across our projects, aimed squarely at making systems legible and safe for agents to operate. State machines are one of its themes. And it was working on exactly that piece that made me see the limit of the frame most teams reach for first.
XState is an excellent tool. I want to say that clearly, because what follows is not a takedown.
It gives developers a practical, expressive way to model behavior — states, events, guards, actions, actors. For most UI flows and application-level workflows, that alone is a large step up from the usual reality: state scattered implicitly across components, hooks, services, and someone’s memory of how the thing is supposed to work. If you’ve replaced that fog with an explicit machine, you’ve already won something real.
But large systems expose a second layer of difficulty. And it’s exactly the layer AI coding tools walk straight into.
When you point Claude Code or Codex at a serious codebase, the interesting question is rarely “can the agent edit the application logic?” It can. It’s good at that now. The harder question is “can it preserve consistency across the entire system?” And that question does not live inside the application runtime. It lives at the boundaries between subsystems — the seams where most real damage happens.
A workflow is never just an API
This is the part that quietly breaks the “application state machine” framing. A real workflow rarely lives inside an API.
It touches infrastructure. It touches the database. It touches schema migrations. It touches permissions. It touches background jobs. It touches external services. It touches deployment pipelines. It touches observability and audit logs.
So if your state machine is beautifully AI-readable but stops at the edge of the application runtime, you’ve made the easy 20% legible and left the expensive 80% as fog. The agent can reason crisply about the part you modeled, and improvise blindly about everything the workflow actually depends on.
For a small system, an API-level state machine may genuinely be enough. The blast radius of a mistake is small, and a human is usually close enough to catch it. For a large system, that stops being true. The workflow contract has to reach further — connecting application logic, infrastructure, database schema, deployment safety, and operational evidence — or the agent will keep producing changes that are locally correct and globally unsafe.
This isn’t a state machine problem. It’s a boundary problem.
Locally correct, globally unsafe
This is the failure mode I keep watching for, because it’s the one that survives “the model got smarter.” A more capable model writes a better change to the file in front of it. It does not, on its own, see the file it isn’t looking at.
So you get changes like these:
- It updates an API handler — but not the database migration that handler now assumes.
- It adds a new state — but not the authorization boundary that state quietly implies.
- It introduces an event that is valid TypeScript — but unsupported by the durable workflow runtime that has to execute it.
- It modifies a deployment flow — but drops the evidence the audit trail is required to keep.
- It generates code that compiles cleanly in one package — and silently breaks the type assumptions in another.
Every one of these is a locally correct change. The model is not wrong about the code it can see. It’s blind to the contract that ties that code to everything else. This is the same thing I argued in *AI as Operator*: the dominant limiter on agentic work is rarely raw model quality. It’s system legibility — and legibility that ends at a subsystem boundary isn’t legibility. It’s luck with a smaller radius.
One change, many boundaries
Imagine a team approves version 7 of a complex workflow on Monday. On Wednesday, one thing has to change.
Without an end-to-end contract, you tell the agent to make the change. It edits what it can see. Did the migration move with it? Did a previously-passing authorization invariant just start failing? Did the durable runtime even accept the new event, or is it a TypeScript fiction that throws at execution time? You can’t tell by looking at the diff, because the diff only covers the layer the agent touched. You’re back to re-reviewing the whole system to trust one change.
With an end-to-end contract, the answer is mechanical. The agent changes one declaration in the workflow contract. The compiler propagates the consequences across every boundary — application, schema, infrastructure, runtime — and tells you what no longer holds. The diff is real. The review is one change, not the whole world.
The agent did the same creative work in both cases. Only the second one is usable on a system you actually have to operate.
Type continuity is the mechanism
Trust here is not a matter of prompting better, or waiting for a model that “just gets it.” A model with perfect reasoning still can’t see across a boundary you never connected. The thing that connects boundaries is type continuity.
The types should not disappear at the seams between the machine definition, the API handler, the database schema, the infrastructure plan, and the runtime trace. If the workflow contract is the source of truth, its types have to flow through the system — not stop politely at the edge of the package they were declared in.
Concretely:
Events are typed. Context is typed. State-specific context is typed. Guards are typed. Reducers are typed. Effects are typed. External actions are typed. Database changes are reflected back into the workflow contract. Infrastructure capabilities are reflected back into the workflow contract.
It looks small in practice. You add one event to the contract:
// one event, declared once — inside the workflow contract
on: {
APPROVE: {
target: 'shipping',
guard: 'hasDeployAuthority', // authorization boundary — typed
reduce: (ctx) => ({ ...ctx, approvedBy: ctx.actor }),
effect: 'recordAuditEvidence', // audit evidence — typed, run by the runtime
description: 'Release approved; handed to the durable workflow runtime',
},
}
And because the workflow contract doesn’t stop at the application boundary, that one declaration forces the rest of the system to answer for itself:
does the durable runtime accept the `shipping` state? → typed / compiled
is there a migration for the new `approvedBy` column? → schema-checked
does `hasDeployAuthority` resolve to a real actor + role? → policy-checked
does `recordAuditEvidence` emit the required evidence? → contract-checked
None of that is a prompt. It is the toolchain refusing to let a locally correct change become globally unsafe.
And notice those four checks aren’t the same kind of check. Two are the type system and the schema. The other two are a policy engine and a contract validator. That’s the honest version of “type continuity”: the types are the spine, not the whole skeleton. Preservation is types and policy and runtime verification and audit, forced to agree against one declaration — the types are what let the rest line up, not what holds it all by themselves.
And the trace that comes out the other end preserves the same semantic structure the compiler understood at authoring time. The meaning that existed when the workflow was written should still exist at runtime, at review time, and at audit time — not be reconstructed by guesswork from logs after something has already gone wrong.
When the types survive the whole journey, “what changed, and what does it break?” becomes a question the toolchain answers. When they don’t, it becomes a question a human re-investigates by hand, every time — which is the exact tax that makes large systems feel un-shippable in the AI coding era.
A library expresses. A compiler preserves.
This is the distinction I keep coming back to, in DSLs and in systems that don’t rot alike.
A library helps a developer express behavior. A compiler helps preserve meaning across boundaries.
None of the pieces here are new — durable runtimes, schema-driven generation, making illegal states unrepresentable. What’s new is the pressure an agent puts on connecting them, and who pays when they aren’t.

A state machine library is a wonderful way to express what an application does. But expression is an authoring-time virtue. The problem AI coding exposes in large systems is not an authoring-time problem — it’s a consistency-over-time, consistency-across-boundaries problem. That’s compiler territory. The source of truth is a structured contract; everything downstream — handlers, migrations, infra plans, traces, authorization checks — is generated or validated against it, deterministically, every time.
This is the same convergence I described for CLI agents: the workflow doesn’t disappear, it becomes invisible infrastructure inside the runtime. The acceptance apparatus is still there. It just stops being the human’s problem to carry across every boundary by hand.
Why this matters more as models improve, not less
There’s a comfortable assumption that a smart enough agent makes all of this unnecessary — that the contract is scaffolding you’ll eventually throw away. I think that’s backwards, for the same reason I’ve argued it before.
As models get better, the authoring gets faster and the changes get bolder. The agent will happily restructure five subsystems at once from a single sentence of intent. That’s precisely when you most need a layer that can answer, deterministically, “and here is everything that just moved, across every boundary, and here is what no longer holds.” A more powerful generator raises the value of the verifier. It doesn’t retire it.
So the bet is not “model vs. structure.” It’s that the teams who give their agents an end-to-end workflow contract — one that carries types and meaning through application, infrastructure, schema, runtime, and audit — will let those agents do more, more safely, while everyone else keeps paying for re-reviews they can’t avoid.
This is the direction the private SDK I mentioned has been built around: treat the workflow contract as the source of truth, keep it deterministic and replayable, and let the meaning the compiler understood at authoring time survive all the way out to the trace. Not because XState is insufficient as a state machine — it’s excellent — but because AI coding on large systems is asking for something with a different shape.
Keeping the contract honest is the hard part
I’ve made the destination sound clean, so let me be honest about the cost. The contract can rot too. Infrastructure drifts; the Terraform state and the thing actually running quietly diverge; a single source of truth spread across database, infrastructure, runtime, and audit is expensive to keep true. Centralize too hard and the contract becomes its own kind of rigidity — a god object every change has to route around.
None of that voids the argument. It is the argument. The reason an end-to-end workflow contract barely exists in the wild isn’t that nobody wants one — it’s that keeping it honest, keeping the map matched to the territory across every boundary, is the actual work. It’s the same thing I keep circling in systems that don’t rot: the destination is easy to draw and expensive to hold. A contract you don’t maintain is just a more confident way to be wrong.
The shift
AI coding does not remove the need for architecture. It raises the cost of architecture that exists only in people’s heads.
The application state machine made behavior legible to humans. That was the right goal for its era, and it’s still necessary.
The workflow compiler makes meaning legible to machines — across every boundary the workflow actually crosses. That’s the goal for this one.
The problem was never that XState falls short as a state machine tool. The problem is that AI coding for large systems needs more than an application-level state machine. It needs an end-to-end workflow contract — one that connects application logic, infrastructure, database schema, type inference, execution traces, and safety boundaries into a single source of truth a machine can be trusted to operate.
Express the behavior. Then preserve its meaning everywhere it has to go.
That second half is the part the AI coding era is quietly demanding — and the part that’s still mostly missing.
메타데이터
- post_id
- 13c1ffdb4a7a
- slug
- beyond-the-application-state-machine-why-ai-coding-needs-a-workflow-compiler-13c1ffdb4a7a
- url
- https://medium.com/@takafumi.endo/beyond-the-application-state-machine-why-ai-coding-needs-a-workflow-compiler-13c1ffdb4a7a
- canonical_url
- https://medium.com/@takafumi.endo/beyond-the-application-state-machine-why-ai-coding-needs-a-workflow-compiler-13c1ffdb4a7a
- author_url
- https://medium.com/@takafumi.endo
- status
- ok
- fetched_at
- 2026-07-15 20:45:36