G
YouTube TikTok Games
Skip to content
mmujtaba0085Public

About

Self-hosted engine for running stateful workflows as persistent actors. Deploy new versions while actors are live — each one routes to the correct version based on its event history, not just its current state. No migration scripts. No downtime.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

StateKeep: an AI agent session waits for a manager to approve an $80 refund; the server is killed and the session is unaffected; a new prompt and policy (v2) is deployed and the waiting session moves to it with its transcript; the manager approves, the refund tool runs exactly once, and the agent replies.

CI Version Node 22+ XState v5 definitions Routing model proved in Lean 4, engine tested against it License: source-available (Apache 2.0 + Commons Clause) Stars

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"].

🤔 Why this exists

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

⚡ See it in 60 seconds

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.

✨ What you get

🧭 History-targeted versioning

Deploy a version for all running actors, or only those whose history is a given path, counted from their first event (historyPath, or per-region historyRegions for parallel states). It's an exact match, not a substring. Chains of pending versions are followed to a fixpoint.

🛟 No silent strandings

Removing a state that actors occupy returns a preview with a confirm token, not a broken deploy. Map states with stateMapping (dotted paths for nested states), reshape context with contextTransform, and rescue stranded actors later.

💾 Durable by default

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).

🔁 Side effects never silently repeated

Each action has a delivery mode (idempotent, verify, …). When an outcome is unknown after a crash, the action is retried safely or sent to a human for review, never fired twice by accident. Read more

🌐 SQLite → Postgres → cluster

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

🤖 AI agent sessions

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

🧪 Know before you ship

Static analysis on every deploy (unreachable states, dead ends, bad targets), scenario tests, and statekeep plan: a terraform-plan-style report of what a release does to production actors, also available as a GitHub Action. Read more

🔌 Bring your stack

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.

🧠 How it works

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")]
Loading
  • 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.

⚖️ How it compares

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.

🚀 Quick start

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, preview

Open 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 events

Windows 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.

🧰 Ecosystem

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

🔬 Evidence it works

  • Proofs: the routing model is machine-checked in Lean 4 with no sorry and 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 chaos and npm run chaos:workflows run 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.

⚙️ Configuration essentials

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.

📚 Documentation

Start here Go deeper Operate
Plain-English overview APV: the routing model Production deployment
Getting started Side effects & delivery modes Cluster mode
Developer guide AI agent sessions Logging
API reference XState compatibility Chaos testing
Scenarios: tickets, loans Migration plan in PRs Importers

❓ FAQ

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.

🤝 Contributing

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.

📄 License

Apache 2.0 with the Commons Clause. Commercial licensing: mmujtaba0085@gmail.com.

About

Self-hosted engine for running stateful workflows as persistent actors. Deploy new versions while actors are live — each one routes to the correct version based on its event history, not just its current state. No migration scripts. No downtime.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages