60-second demo · Quick start · How it works · Compare · API · FAQ
StateKeep runs your workflows as persistent statechart actors (orders, loans, onboarding, support tickets, AI agent sessions) and lets you ship new versions to actors that are already running.
Each actor moves to the right version based on the path it took to get where it is. You don't replay events, write migration scripts, or keep old workers alive.
You define a machine in XState v5 JSON, deploy it with one HTTP call, and send events. StateKeep stores every actor's state, context (encrypted) and full event history in SQLite or PostgreSQL. It survives crashes, and it can run as a cluster.
Tip
The one-sentence idea: two orders can both be shipped, but if only one was ever put on hold, a fix for held orders should reach only that one. StateKeep makes that a deployment flag: "historyPath": ["HOLD", "RELEASE"].
Once a workflow is live, three problems show up.
| Problem | What usually happens | What StateKeep does |
|---|---|---|
| Changing logic for in-flight instances | Hand-written migration scripts that pass staging and fail on years of production edge cases | Every actor is routed to the new version by a deterministic rule, and its state is mapped declaratively |
| Old versions you can never retire | v1 workers stay alive until the last v1 instance finishes | Running actors move forward, and old versions empty out and are collected |
| Instances silently left behind | Actors keep running stale logic in states that no longer exist | Before you commit, a preview lists exactly which actors would be stranded. They're held for rescue, not misplaced |
docker compose up -d # API on :3001, dashboard on /dashboard
sk() { curl -s -H 'content-type: application/json' -H "x-api-key: $STATEKEEP_API_KEY" "$@"; }
# 1. Deploy v1 of an order workflow
sk -X PUT localhost:3001/v1/definitions -d '{"id":"order-v1","definition":{"id":"order","initial":"placed","states":{
"placed":{"on":{"PAY":"paid","HOLD":"on_hold"}}, "paid":{"on":{"SHIP":"shipped"}},
"on_hold":{"on":{"RELEASE":"shipped"}}, "shipped":{"on":{"DELIVER":"delivered"}}, "delivered":{"type":"final"}}}}'
# 2. Two orders reach "shipped" by different paths
A=$(sk -X POST localhost:3001/v1/actors -d '{"definitionId":"order-v1"}' | jq -r .id)
B=$(sk -X POST localhost:3001/v1/actors -d '{"definitionId":"order-v1"}' | jq -r .id)
sk -X POST localhost:3001/v1/actors/$A/event -d '{"type":"PAY"}'; sk -X POST localhost:3001/v1/actors/$A/event -d '{"type":"SHIP"}'
sk -X POST localhost:3001/v1/actors/$B/event -d '{"type":"HOLD"}'; sk -X POST localhost:3001/v1/actors/$B/event -d '{"type":"RELEASE"}'
# 3. Ship v2 (held orders need an audit before delivery), only for orders that were held
sk -X PUT localhost:3001/v1/definitions -d '{"id":"order-v2","parentId":"order-v1","historyPath":["HOLD","RELEASE"],
"definition":{"id":"order","initial":"placed","states":{
"placed":{"on":{"PAY":"paid","HOLD":"on_hold"}}, "paid":{"on":{"SHIP":"shipped"}},
"on_hold":{"on":{"RELEASE":"shipped"}}, "shipped":{"on":{"DELIVER":"audit"}},
"audit":{"on":{"APPROVE":"delivered"}}, "delivered":{"type":"final"}}}}'
sleep 2 # the migrate worker moves B in the background
sk localhost:3001/v1/actors/$A | jq .definitionId # "order-v1" (paid normally: untouched)
sk localhost:3001/v1/actors/$B | jq .definitionId # "order-v2" (was held: now gets the audit step)Both orders were in the same state. Their histories differed, so they got different versions, with no replay and no script.
|
Deploy a version for all running actors, or only those whose history is a given path, counted from their first event ( |
Removing a state that actors occupy returns a preview with a confirm token, not a broken deploy. Map states with |
|
An event is acknowledged only after it's committed. Actors survive crashes, restarts and deploys. Context is encrypted with AES-256-GCM, and the event history is an append-only audit log (unless you turn on optional pruning). |
Each action has a delivery mode ( |
|
Start with one SQLite file. Switch to PostgreSQL, then run several API servers behind any load balancer. Each actor has one owner, requests are forwarded, and a failed owner is taken over safely. Correctness never depends on clocks or timing: a paused or slow server can't commit stale state or route past a deployment (fencing tokens checked at commit), and timers run on the database's time line. Read more |
Run an agent loop as an actor with tool calls with retries, human approval states, and live prompt/policy upgrades for sessions already running. Read more |
|
Static analysis on every deploy (unreachable states, dead ends, bad targets), scenario tests, and |
Guards and actions in any language over HTTP, SDKs for TypeScript, Python and Go, importers for AWS Step Functions, SCXML and BPMN, webhooks, WebSocket streams and a dashboard. |
flowchart LR
E["Event for actor a"] --> R{"Routing rule (APV):<br/>earliest applicable change-point<br/>for a's family, time and history"}
R -- "none" --> P["Process the event<br/>on a's current version"]
R -- "change-point c" --> M["Migrate: map state (μ)<br/>and context (τ) to c's version"]
M --> R
M -- "state can't be placed" --> S["Stranded: held for rescue,<br/>never misplaced"]
P --> C[("Commit state + event<br/>then acknowledge")]
- Every deployment registers a change-point: from logical time t, actors of this family whose history is π go to version d′. π is either an exact history, the actor's whole transition history from its first event (not a substring), or "all actors".
- Every actor carries a history fingerprint, a length-prefixed SHA-256 chain over the events that changed its state. Storage is constant per actor (one fingerprint, plus one per parallel region) and never grows with the history; routing never scans the log. The routing engine indexes the first 64 bits of the fingerprint, and every hit is confirmed against the full 256-bit value, so two histories that share a 64-bit key are never confused (details).
- Before each event, the actor is routed to a fixpoint through every pending version. Then the event runs.
- A migration places the actor in its landing state without running entry actions and without replay, so it never fires side effects (details).
This scheme is called APV (anchor-point versioning). The core properties of its routing model are machine-checked in Lean 4 (formal/APV.lean):
- determinism and progress (routing terminates and never revisits a version);
- completeness;
- safety (an actor is never placed in a state that doesn't exist);
- the parallel-region order;
- the garbage-collection frontier.
The proofs are about the model, not the shipped engine (a compiled WebAssembly module). The engine and the routing code are differentially tested against those same Lean definitions run as a program (test/test_model_oracle.js): the model is proved, and the code is tested against it.
| StateKeep | Temporal | Camunda 8 | XState (library) | |
|---|---|---|---|---|
| Workflow model | Statechart (XState v5 JSON) | Code | BPMN | Statechart |
| How in-flight instances get a new version | Routed automatically on their next event, per instance, by history | Keep running the old code; changes via patching/versioning APIs in your code | Explicit migration plans applied to chosen instances | You build it |
| Choose instances by the path they took | ✅ Built in (historyPath) |
Manual (code branches) | Manual (instance queries) | Manual |
| Preview who'd be stranded before deploying | ✅ With confirm token | — | Validation of the migration plan | — |
| Replays history to restore state | No: state is stored | Yes (deterministic replay) | No | No (snapshots) |
| Minimum to self-host | Node 22 + one SQLite file | Temporal server + database | Orchestration cluster + secondary storage | Your app |
When to pick what:
- Temporal if your workflow is code and replay fits.
- Camunda if BPMN and process modelling teams are the center of gravity.
- StateKeep if the workflow definition is the product: you need to version it as data, change it for instances already running, and audit it as plain database rows.
npx (fastest: one command, Node.js 22+)
npx statekeep serve # SQLite in ./statekeep-data, API + workers on http://127.0.0.1:3000
curl localhost:3000/v1/health # {"status":"ok",...}
npm install @statekeep/sdk # client: deploy, spawn, send, previewOpen http://localhost:3000/dashboard/. For anything beyond a local trial, set
STATEKEEP_ENCRYPTION_KEY (openssl rand -hex 32) and STATEKEEP_API_KEY first; --port,
--host and --data change the defaults, and STATEKEEP_DB_URL=postgres://… switches to PostgreSQL.
Docker
git clone https://github.com/mmujtaba0085/StateKeep && cd StateKeep
cp .env.example .env
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # paste as STATEKEEP_ENCRYPTION_KEY
# also set STATEKEEP_ADMIN_KEY and STATEKEEP_API_KEY in .env
docker compose up -d
curl localhost:3001/v1/health # {"status":"ok",...}Then open http://localhost:3001/dashboard/ in a browser.
Node.js (22+)
npm install
cp .env.example .env # set the three keys as above
npm start & # API server
npm run migrate-worker & # background migrations
node --env-file=.env src/workers/scheduler-worker.js & # timers / delayed eventsWindows works natively. Run npm install from PowerShell, not WSL.
Embedded (no server)
Use StateKeep in-process via statekeep/lib (example). Or add history-based version routing to plain XState v5 apps with @statekeep/xstate-apv, which wraps XState's own persist/restore.
Code-first workflows
Write workflows as async functions with @statekeep/durable. Runs survive crashes, and runs already in progress move to new code versions based on the steps they've completed.
| Piece | What it's for |
|---|---|
sdk/ · sdks/python · sdks/go |
Client SDKs (TypeScript, Python stdlib-only, Go) |
packages/durable |
Code-first durable workflows on top of StateKeep |
packages/xstate-apv |
History-based version routing for XState v5, no server |
statekeep CLI |
push, preview, plan, import, dev (file watcher) |
| GitHub Action | Comments a migration plan on every pull request |
| Importers | AWS Step Functions (ASL), SCXML, BPMN → StateKeep |
| Remote implementations | Guards and actions in any language over HTTP |
| Dashboard | /dashboard: actors, versions, deployment preview, queues, metrics |
-
Proofs: the routing model is machine-checked in Lean 4 with no
sorryand only standard axioms (formal/). This covers the model, not the engine binary. -
Model-vs-code testing: the live routing code, the deploy-preview code and the analysis driver are compared hop-by-hop with the Lean definitions on random registries.
-
Fault injection:
npm run chaosandnpm run chaos:workflowsrun generated workflows while they:- kill API servers, workers and services;
- deploy new versions mid-flight;
- fire storms of concurrent, duplicate and unknown events.
Every actor is then checked against a ground-truth model kept independent of the system under test: no lost or duplicated events, no misrouted or misplaced actors. The full campaign (
scripts/chaos-campaign.sh) runs on SQLite, PostgreSQL and a 2-server PostgreSQL cluster, at normal and hard intensity. The latest run covered 2,600 actors and 54 live deployments (2,417 migrations checked), with 42 API-server kills, 21 database-connection drops, 29 service crashes and 52 worker kills: 0 violations (results, raw summary). All runs were on a single host with generated workflows. -
Test suites: unit, property-based, integration, statechart (SC1–SC18), cluster and end-to-end. Run them with
npm run test:all.
| Variable | Purpose |
|---|---|
STATEKEEP_ENCRYPTION_KEY |
64 hex chars, AES-256-GCM key for actor context (required) |
STATEKEEP_API_KEY |
Required x-api-key for the API. Without it, the server binds to 127.0.0.1 only |
STATEKEEP_ADMIN_KEY |
Admin endpoints (x-admin-key) |
STATEKEEP_DB_PATH / STATEKEEP_DB_URL |
SQLite file, or postgresql://… |
STATEKEEP_CLUSTER=true |
Several API servers on one Postgres (guide) |
STATEKEEP_REMOTE_URL |
Your service implementing guards and actions in any language |
Everything else is in .env.example and docs/deployment.md.
What is StateKeep, in one line?
A self-hosted engine that runs statechart workflows as persistent actors and migrates the ones already running to new versions, chosen per actor by the path it took.
Do I need to write migration scripts?
No. Added states and changed transitions need nothing. Renamed or removed states need a declarative stateMapping. New context fields need a contextTransform (copy, constant or default). The preview tells you which you need before anything is written.
What happens to an actor sitting in a state the new version deletes?
If there's no mapping for it, the deploy returns requires_confirmation with the affected actors and a confirm token. If you confirm, those actors are marked needs_rescue. The confirmation is re-checked right before the commit: an actor that wasn't in your preview gets you a fresh preview, never a silent stranding. They're never moved into a wrong state. A later rescue deployment (or a periodic sweep) moves them forward.
How is this different from Temporal's versioning?
Temporal keeps in-flight workflows on the code they started with and lets you branch with patching or versioning APIs inside your code. StateKeep versions the definition as data and moves running instances forward automatically. You choose which instances by their history, without replay.
Does it work with my existing XState machines?
Yes. Definitions use the XState v5 JSON format, and the runtime is StateKeep's own interpreter. See XState compatibility. For in-app XState with no server, use @statekeep/xstate-apv.
Is it production-ready?
It's at v1.1, with extensive automated testing (proofs, model-vs-code tests, fault injection, multi-server tests). Evaluation so far has been on single hosts with generated workflows. If you run it in production, we'd love to hear from you, so please open an issue.
Is it open source?
No, it's source-available. The code is public under Apache 2.0 with the Commons Clause, which is not an OSI-approved open-source licence. You can use, modify and self-host it for any purpose, but selling StateKeep, or a modified version, as a hosted service or product needs a commercial agreement. The APV routing engine ships as a compiled WebAssembly module whose C source is not public. Its routing model is published and proved in formal/, and the engine is tested against that model.
Issues and pull requests are welcome. Read CONTRIBUTING.md, then run npm run test:all. If you're evaluating StateKeep for a real workload, open an issue describing it. Real-world cases shape the roadmap.
If StateKeep is useful to you, a ⭐ helps other people find it.
Apache 2.0 with the Commons Clause. Commercial licensing: mmujtaba0085@gmail.com.