Model or dataset
statelyai/agent avatar
statelyai/agent

Stately Agent: LLM agents built as XState state machines

Create state-machine-powered LLM agents using XState

468 stars22 forksTypeScriptMIT

At a glance

What is it?
Stately Agent puts agent control flow inside an XState machine, so the model can only propose events the machine already allows. It is in 2.0 alpha, ESM-first, and needs Node 22.18 or newer.
Who is it for?
Adopt Stately Agent if your agent's control flow is already the part that keeps breaking: retries, approval gates, bounded tool calls, and runs that must survive a restart. Skip it if you want a framework that owns prompts, memory, and storage for you, because this package deliberately does not.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Stately Agent solves: models choosing actions they were never allowed to take

Most agent code is a loop. You call a model, parse its output, branch on it, call a tool, append to a message list, and repeat until some condition you wrote by hand says stop. The prompt is the specification and the loop is the enforcement. When the model returns a tool name that does not exist, or an action that is syntactically valid but semantically wrong for the current step, nothing in the architecture stops it. You add validation after the fact.

Stately Agent inverts that. The README states the machine owns control flow and the model only ever picks a legal event. Concretely: the machine defines what the agent can do, your application chooses the model and stores state, and the model proposes an event that the machine then accepts or rejects. The tagline is blunt about the goal, "Make invalid agent actions impossible."

The audience is narrower than the pitch suggests. This is for TypeScript teams who already think in statecharts, or who have written an agent loop and watched it grow a retry counter, an approval flag, and three booleans that should have been states. It is not for someone who wants a batteries-included agent framework with memory, vector storage and a hosted dashboard. The package ships a state machine integration and executors, and the README is explicit that the machine never talks to a model directly.

How the machine, the executor and the model pass control between each other

The architecture diagram in the README shows a four-hop cycle. The agent machine (states, guards, requests) emits a request to runAgent. runAgent calls a host executor, which for the AI SDK path is generateText, streamText, or decide. That executor makes the API call. The model returns a result, the executor hands it back to runAgent, and runAgent delivers an event or a final output to the machine.

The separation matters because the executor is a plain function. The README's quickstart runs a full agent with no API key by supplying executors inline, and the same machine later runs against a real provider by swapping in createAiSdkExecutors. Core does not import the AI SDK. Swapping createAiSdkExecutors for createScriptedExecutors changes nothing about the agent definition, which is what makes offline testing of agent logic possible at all.

Requests are typed. setupAgent takes context, input, output and an events map built from Zod schemas, and the invoke block calls src: "agent.decide" with a model reference, a system prompt, a prompt, and an allowedEvents array. The model's choice is constrained to that array before it is ever considered by a transition. A defineModels registry passed to setupAgent({ models }) types the machine's model refs and supplies the AI SDK executor by default, with explicit executors overriding it.

State is native XState. The README points at a snapshot-migration example that uses XState's own version and migrate contract, and a file-snapshot-store example that persists native snapshots in application code. Resumability is therefore not a feature this package implements; it is a property you get from XState plus wherever you decide to write the snapshot.

Installing Stately Agent and running a refund agent without an API key

The README gives the install command for the prerelease channel, and it lists xstate and zod as peers, so both go in the same command. Node 22.18 or newer is required, along with XState v6 alpha.46 or newer. The package is ESM-first, though CommonJS builds are published so require() works.

bash
pnpm add @statelyai/agent@alpha xstate@alpha zod

Optional executors are separate installs. The Vercel AI SDK path needs ai@^7 and @ai-sdk/openai@^4, and the raw OpenAI SDK path needs openai.

bash
pnpm add ai@^7 @ai-sdk/openai@^4

The quickstart builds a refund-review agent. The model may propose AUTO_REFUND or REVIEW, but the AUTO_REFUND transition carries a guard that only resolves when context.amount is at most 100. Below that limit the run reaches the refunded final state and prints an outcome; above it, the guard returns undefined and the decision is tried again. Note the inline executor: it returns a fixed event, so the example needs no provider package and no key.

ts
import { runAgent, setupAgent } from "@statelyai/agent";

const result = await runAgent(refundMachine, {
  input: { request: "I was charged twice for the same order.", amount: 75 },
  executors: { decide: async () => ({ event: { type: "AUTO_REFUND" } }) },
});

if (result.status === "done") {
  console.log(result.output);
}

To go live, the machine does not change. Only the executors do, and the README shows createAiSdkExecutors({ models: { fast: openai("gpt-5.4-mini") } }) as the replacement. For machines with several requests, createScriptedExecutors holds ordered answers keyed by request name.

Where the guard-rejection loop can bite you

The refund example is the honest place to look for the failure mode. When the model picks AUTO_REFUND for an amount over 100, the guard rejects the choice and, per the README, the decision is tried again. That is a retry loop whose termination depends on the model eventually choosing something else. The documentation does not state a retry ceiling, a backoff, or what happens if the model keeps proposing the same rejected event. A machine author can model that explicitly with a counter in context and a transition on exhaustion, but the package does not do it for you, and the quickstart does not show it.

The second constraint is version coupling. The README warns that provider packages must match your ai major, that @ai-sdk/openai@^4 pairs with ai@^7, and that a bare @ai-sdk/openai resolves to @latest and can mismatch the ai peer. That is a real dependency trap in a monorepo where another package pins a different ai version.

The third is maturity. The published version is 2.0.0-alpha.24 and the README states plainly that Stately Agent 2 is in alpha and APIs may change before the stable release. The repository's most recent push was on 2026-09-08, and the release train has been shipping alpha increments through August 2026, so the churn is active rather than dormant. If you need a frozen API surface today, this is the wrong tool. There is also a scope boundary worth stating: if your agent is genuinely a single model call with no branching, wrapping it in a statechart adds a layer and buys nothing.

How this differs from a hand-rolled while loop and from LangGraph

The README's own migration path is the fairest comparison, because it is the thing most readers already have. In a while loop, the SDK calls, tools and retry code are the program, and control flow is implicit in the order of statements. The from-a-loop guide describes the retrofit as the inverse: your SDK calls, tools and retry code become executors, and the machine replaces only the control flow. Nothing about how you call the model changes. What changes is that the set of legal next steps becomes a value you can inspect, test and draw, rather than a sequence you have to read.

Against LangGraph, the difference is representational. LangGraph models an agent as a graph of nodes and edges with shared state, which is close enough that the two get compared often. XState's model is hierarchical states, guards on transitions, and native snapshots with a version and migrate contract. The practical consequence is that nested and parallel behaviour, like an approval substate inside a running task, is expressed as state nesting rather than as additional nodes and routing. Whether that is better depends on whether your team already reads statecharts. A team with no XState background will spend its first week on the machine semantics, not on the agent.

The third alternative is doing nothing structural and relying on schema validation of model output. That catches malformed events. It does not catch a well-formed event that is illegal at this point in the run, which is the case the guard exists for.

Licence, maintenance and what an upgrade actually costs

The package is MIT licensed, which permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is the extent of what the repository states; questions about your own distribution obligations belong with your counsel, not with a README.

The maintenance signals are mixed and worth reading separately. The repository is not archived. The last push was on 2026-09-08, which is recent enough that the project is being worked on. Releases are frequent but all on the alpha line: 2.0.0-alpha.20 on 2026-08-21, alpha.21 on 2026-08-27, alpha.22 on 2026-08-31, and the package.json in the repository reads 2.0.0-alpha.24. Alpha numbering with that cadence means the upgrade cost is not a one-time migration. Every bump is a chance for a breaking change in setupAgent, the executor interfaces, or the subpath exports, and the README says as much.

Structurally, the upgrade surface is smaller than it looks. The package exports subpaths for the core, ./ai-sdk, ./machines, ./openai and ./otel, and ships dist, schemas and skills. Because the machine definition is decoupled from the executors, a breaking change in the AI SDK executor should not touch your machine, and vice versa. The snapshot format is the part to watch: it is XState's, with its own version and migrate contract, so snapshot compatibility tracks XState's alpha line rather than this package's.

Editorial conclusion

Adopt Stately Agent if your agent's control flow is already the part that keeps breaking: retries, approval gates, bounded tool calls, and runs that must survive a restart. Skip it if you want a framework that owns prompts, memory, and storage for you, because this package deliberately does not. Before committing, verify three things: that your XState version satisfies the v6 alpha.46 peer requirement, that your provider package major matches your ai major, and that you can accept alpha API churn, since the published line is 2.0.0-alpha.24 and the README states APIs may change before the stable release.

Frequently asked questions

What Node and XState versions does Stately Agent require?

The README states Node 22.18 or newer and XState v6 alpha.46 or newer. The package is ESM-first, though CommonJS builds are published so require() works.

How do I install Stately Agent with the AI SDK executor?

Install the core package with its peers first, then the AI SDK packages. The README gives pnpm add @statelyai/agent@alpha xstate@alpha zod and pnpm add ai@^7 @ai-sdk/openai@^4.

Can I run a Stately Agent machine without an API key?

Yes. The quickstart passes an inline executors object whose decide function returns a fixed event, so the run needs no API key and no provider package. The same machine later runs against a real model by swapping in createAiSdkExecutors.

What happens when the model proposes an event the machine does not allow?

The guard rejects the choice and the decision is tried again, as described in the refund example. The README does not document a retry ceiling or backoff, so a machine that must bound retries has to model that itself.

Is Stately Agent stable enough for production?

The published version is 2.0.0-alpha.24 and the README states that Stately Agent 2 is in alpha and APIs may change before the stable release. The repository is not archived and the last push was on 2026-09-08, so development is ongoing.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. statelyai/agent on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/statelyai-agent.svg)](https://hysenlabs.com/projects/statelyai-agent)