Draw the flow.
Ship the graph.
OpenStateGraph is a framework built on top of LangGraph and LangChain: a
document format, a compiler for it, and the node semantics it emits — organised by
atomic design. It compiles rather than interprets, so what
you draw becomes a plain LangGraph StateGraph — real Python that runs anywhere
Python runs, with or without this editor.
A workflow platform you can walk away from
The closest-looking tools own their execution engine: a flow runs inside their
platform, through their runtime, or it does not run at all. Your logic lives as a blob
in someone else's database. OpenStateGraph has a single job — turn a canvas into
workflow.json, and turn workflow.json into a StateGraph.
| Approach | Who executes your flow | Where your work lives |
|---|---|---|
| Own your executor n8n, Dify, Langflow, Flowise |
Their engine, their platform | A record in their database |
| Multi-runtime abstraction | A lowest-common-denominator layer | A spec no runtime fully honours |
| Single-target compiler OpenStateGraph → LangGraph |
LangGraph, directly | Files in your git repository |
Your workflow is a file in git
workflow.json plus the package's own tools/,
skills/, knowledge/, functions/ and
tests/. Reviewable in a pull request. Diffable. Yours.
The output runs without the editor
The compiled graph is an ordinary Python object: import it from a script, exercise it
with pytest, deploy it wherever Python runs. Delete this repository and
your workflow still runs.
Checkpointing, time travel, interrupt() for human-in-the-loop,
Send fan-out, reducer merging and token streaming are inherited from
LangGraph — never reimplemented. The compiler is not portable; the output is.
It is a framework, not just an editor
One wheel: a document format, a compiler for it, and the node semantics the compiler emits. The canvas, the HTTP API and the MCP layer are optional surfaces over those three — none of them is on the path of a workflow running in your service.
$ uv tool install "openstategraph[server,ollama]"
$ openstategraph run ./my-workflow "How many invoices are there?"
On PyPI, as a pre-release — the wheel is built and CI installs it into an
empty virtualenv outside the repository on every pull request. The version is named
in full because pip and uv skip pre-releases in an unpinned
requirement; there is no index to name, and the pin goes away with the first final
release.
A dependency list short enough to read
The core is four declared packages — langgraph, langchain,
langchain-core, pydantic — resolving to 36
distributions, measured in a clean virtualenv. Everything else is an extra you ask
for by name: a provider, [deep], [sqlite],
[server], [mcp].
You never install a web server to run a graph in your own process. A test asserts
load_workflow leaves fastapi, uvicorn,
deepagents and mcp out of sys.modules
entirely.
Atoms
Inputs · Tools · Output. One thing, made of nothing else: a source with no logic, one capability bound to an agent, a sink with one input and no decision.
Molecules
Reasoning & control. One decision step each — agent, router, grader, approval, supervisor, worker, format-report. A supervisor alone is a molecule; supervisor + workers + join is an organism you draw, not one you drag.
Organisms
Composition. A whole workflow — its own nodes, state and loop — mounted as one step. The same ladder runs through the code: Interface → Abstract → Base → Concrete.
One direction: canvas → workflow.json → StateGraph
The seam is deliberately one-way. Nothing reads runtime objects back into the model,
expressions are a serialisable JSON AST rather than host-language lambdas, and LangGraph
type names never leak into your document. That is what keeps workflow.json
the vendor-neutral layer.
# No editor. No platform. No runtime of ours.
from openstategraph import load_workflow
workflow = load_workflow("workflows/chinook-assistant")
if workflow.warnings: # what did not wire, never silence
print("degraded:", workflow.warnings)
answer = workflow.ask("Top 5 genres by revenue?", thread_id="t1")
print(answer) # it IS the answer string
print(answer.decisions) # …and which branch each router took
# .graph is an ordinary LangGraph object — the escape hatch:
print(workflow.graph.get_graph(xray=True).draw_mermaid())
The same document drives the editor's canvas and the Python compiler, and the two agree because the contract between them is a generated, committed artifact rather than a convention: the port table comes from the TypeScript node catalogue, the OpenAPI document comes from the Python app, and CI regenerates both and fails on a diff.
Five things that only work because it compiles
A real compiler
Canvas to StateGraph: agents become create_agent loops,
routers become conditional edges, teams become subgraphs, fan-out becomes
Send. Retry, timeout and cache policies compile to graph assembly, where
LangGraph actually puts them.
A second brain per workflow
Knowledge is built from your real sources, then chunked: one index line per topic, free to carry — the full doc fetched only when an agent commits to that direction. Generated, then owned by you: the first keystroke claims a doc, and a stale badge tells you when the source behind it moved.
Teams you can drill into
A team node is a whole workflow behind one card — supervisor plus workers, composed as a subgraph. Open it and you are editing this mount: the package is the class, the mount is the instance, and your change is stored as an override on the parent — the child package's bytes never move, and other mounts of it are untouched. Subagents stay isolated by design: they receive a task and report a result, never the parent's history.
Humans in the loop
Approval nodes compile to LangGraph's interrupt(), so a paused run is a
checkpoint you can resume — not a thread parked in memory. Runs stream back token by
token, and every spawned worker or subgraph announces itself as it starts.
Stop really stops the server, not just your tab: no further supersteps are
scheduled, though work already dispatched still finishes.
Your own LLM can compose the graph — over MCP
A workflow is data, and a compiler validates data without running anything. So the whole
surface is exposed over the Model Context Protocol: your client calls
get_node_vocabulary() for the exact node types and port contracts, composes
a document with your model, and calls compile_workflow() for a
deterministic verdict. Invalid documents come back as findings and no
artifacts — that verdict → revision loop is the product. It is stateless, calls no model
of ours, and a deployment needs no provider key at all.
Valid documents come back as a committable workflow.json envelope, a run
snippet, the package layout, and the Mermaid the compiler actually produced —
which clients like Claude render as a diagram on your machine. Drafts in, humans
publish: there is no publish tool and no delete tool over MCP, by design.
One example, small enough to read in a glance
It ships as a real package under workflows/, with its own tools, tests,
evals and data. Five intents, three destinations, one answer — all in one document.
Chinook Assistant · chinook-assistant
A question, a Router that sorts it into five intents, and three places it can land: a Data Analyst for anything the Chinook database can answer, a tool-less Front Desk for greetings, off-topic asks and general knowledge, and a Web Researcher holding web search and web fetch. Thirteen nodes, left to right, nothing to untangle and nothing hidden in another file.
Data Analyst · the data_query branch
Natural language to SQL over the Chinook music store. One agent bound to three
read-only tools (list tables → table schema → run query), behind a grader that
sends a bad answer back for another attempt — up to three — and a per-table second
brain of eleven knowledge docs, so the model learns what InvoiceLine
means before it writes a join instead of guessing from a schema dump. Its prompt field
is empty: its rules arrive over the skill port from a Markdown file wired
in beside it, which is how you customise a prebuilt node without editing it.
There is exactly one sample database, redistributed with the repo, and exactly one visible example evaluated against it — so every figure it produces is checkable against the same file. The backend defaults to a hosted model, so it needs no API key to try.
Three steps to a workflow that is yours
Nothing here needs an account, a hosted control plane or a credit card. Fifteen minutes from clone to a run streaming across the canvas.
Run it
One command brings up the editor and the Python runtime, seeded with working examples.
Quickstart →Read a real one
Open the Chinook Assistant, follow an intent from router to grader, then change a rule and re-run.
Examples →See what it becomes
One workflow followed all the way down: the file, the plan, the compiled graph, the prompt a model receives.
Behind the scenes →Add your own
Register a node type or a tool, keep core/ untouched, ship the compiled graph.
From clone to a streaming run
Prerequisites: Node 20+ (developed against 22), Python 3.11+ (developed against
3.12), and a model to call — an Ollama cloud account by default, or an
ANTHROPIC_API_KEY / OPENAI_API_KEY if you have one. Nothing runs
locally on your GPU; nothing is required to open the editor.
Only want to use it? Then you do not need the clone at all: the wheel carries
the built editor, so pip install plus openstategraph serve is
the whole thing — step 0. The clone is for working on OpenStateGraph itself.
-
Step 0 — or skip the clone entirely
The wheel carries the canvas. The built editor ships as package data, so one install and one command give you the whole product from a single origin: the canvas at
/, the customer chat at/chat, the API under/api. No clone, no Docker, no Node.$ uv tool install "openstategraph[server,ollama]" $ openstategraph serve --open # prints the URLs it landed onNo
--porttakes 8000, or the next free port if 8000 is busy;--port Nmeans exactly N and says so if it is taken;--port 0lets the OS choose. It binds127.0.0.1by default — this process holds your API keys and has no authentication.Only want to run a graph? Then you need none of the web layer. A workflow package is a folder with a
workflow.jsonin it, and exit codes are fixed (0ok,1failure,2usage,3a missing extra), sovalidateworks as a CI gate.$ uv tool install "openstategraph[server,ollama]" $ openstategraph new my-workflow $ openstategraph validate ./workflows/my-workflow $ openstategraph run ./workflows/my-workflow "Say hello." # a fresh scaffold knows nothing yetThis is a pre-release, and that is the whole reason for the==. The wheel is built and verified — CI installs it into an empty virtualenv outside the repository and runs a workflow there on every pull request — and it is on PyPI, so the command above names no index. Until0.3.0rc18it named three: the build lived on TestPyPI, its dependencies on PyPI, anduvhad to be told it could mix them. Those are gone. The exact version stays until a final release exists, because pip anduvskip pre-releases in an unpinned requirement — and everything below is unchanged either way. -
Clone the repository
Everything — editor, runtime, example workflows and their sample database — is in this one tree.
$ git clone https://github.com/zulfeekar/openstategraph.git $ cd openstategraphThe repository is private right now. This clone, and everygithub.com/zulfeekar/openstategraphlink on this page — docs, Contributing guide, Changelog, issues — 404s for anyone who is not a collaborator. Nothing on this page will change that quietly; it changes when the repository opens up, and this line goes with it. -
Start both stacks
One entry point.
./start devruns Vite anduvicornwith hot reload; plain./startbuilds the production Docker stack instead and serves everything from port 8000.$ npm install $ ./start dev # editor :5273 · runtime :8000 · ./start stopTwo processes on purpose. A Vite dev server and a Python ASGI server have little in common to unify. The editor opens read-only without the backend — saving workflows and running them need the runtime up. -
Open the editor
http://localhost:5273opens on a blank canvas that offers the workflows this project holds. Set one provider credential before you press Run — a run reaches the backend, and it is refused without one. -
Run the Chinook Assistant
Open Chinook Assistant from the workflow picker (the package is
workflows/chinook-assistant) and press Run. It sorts the question into one of five intents and takes exactly one of three branches: the Data Analyst, the tool-less Front Desk, or the Web Researcher. Tokens and active nodes stream back onto the canvas as it goes.$ python -m pytest workflows/chinook-assistant $ openstategraph graph ./workflows/chinook-assistant # Mermaid text, no network call $ openstategraph run ./workflows/chinook-assistant "Revenue by genre?"Then follow the data branch.data_queryleads to the analyst — one agent, three read-only Chinook tools and a grader that makes it try again. Small enough to hold in your head, and every number it produces is checkable against the one sample database. -
Open the chat
http://localhost:8000/chatis the end-user surface: no canvas, no node inspector, just a conversation that dispatches to your published workflows. It is the same process and the same workflows directory as the editor — Publish is the one human action that makes a workflow visible there, and a saved draft never is.
One name, one place: the clone URL above and every
github.com link on this site are written by
scripts/build_site.py from [project.urls] in
backend/pyproject.toml. Renaming the repository is one line there and
--write; CI fails if the two disagree.
Two people, two surfaces, one document
The person who builds a workflow and the person who uses one want opposite things. They
get different windows onto the same workflow.json — the canvas is not a
demo of the chat, and the chat is not a stripped-down canvas.
The editor at :5273
- 1Edit. Drop nodes, wire typed ports, write the rules half of a prompt — the machinery half stays read-only beside it.
- 2Run. Press Run and watch the real graph execute: active node highlighting, streamed tokens, a checkpoint at every approval node.
- 3Publish. A workflow is a draft until you publish it; publishing is what puts it in front of end users.
Then commit the package. It reviews like code because it is code.
The chat at :8000/chat
- 1Pick a workflow — the picker lists the published ones only, so a half-built draft can never answer a customer.
- 2Or leave it on Auto, and a concierge routes the question to whichever published workflow claims it.
- 3Watch it work. The run streams back with a live diagram of the graph, and approvals surface as a prompt in the conversation.
No canvas, no node types, no vocabulary to learn. Just the answer, and the ability to see how it was reached.
published: false; the chat only ever lists published workflows, so a draft
cannot answer a customer. There is no publish tool over MCP and no way to automate it —
a machine may compose and propose, a human looks at the graph and decides it may talk to
people.
You extend by registering, never by editing the engine
Every extension point is a registry. Adding a capability is a registration — a new node
type, tool, model provider, connection rule, validation check or canvas behaviour lands
without touching core/. That is the rule that makes contributions reviewable
and merges boring.
The contribution shape
- A node type is one file — model, field schema, ports, executor — plus its registry entry.
- A workflow is a directory:
workflow.jsonandAGENTS.mdrequired;tools/,functions/,tests/,data/discovered by convention. - Configuration is declared once as a field schema; the card, inspector, defaults and validation all derive from it.
- Docs land with the code. CI fails a pull request that changes source without touching documentation.
Before you open a pull request
npm run verify # tsc + eslint + prettier + vitest
python -m pytest # backend + workflow tests
TDD is the house style, and core/ is pure TypeScript with no excuse for
untested logic. The architecture contract is written down: most rejected proposals are
rejected by a rule that already exists in CLAUDE.md. Read the
non-negotiables before designing anything.
Ten questions, ten places to look
The deep material lives in the repository, next to the code it describes — so it is reviewed like code and cannot quietly drift. These are the entry points.
What this actually is
Start here if you are deciding. The framework sentence, the atomic-design tiers, how the adoption interface compares to other Lang* frameworks, what it refuses to own, and when not to use it.
How to start
Clone, ./start dev, open the Chinook Assistant, ask it something in
/chat. Prerequisites, the two model paths, and why no API key is needed for
a first answer.
How to use it in your project
The three consumption modes — fork/checkout, artifact, MCP — with exact commands, the
upgrade friction stated honestly, what artifacts you own, and the draft → Publish →
/chat story.
What can be taken away from me
The stability contract: three tiers of surface, a signature-snapshot test, the
workflow.json version policy, the CLI's fixed exit codes, and the
deprecation rules that apply before and after 1.0.
What to build, and how to arrange it
The seven agentic patterns — augmented LLM, prompt chaining, routing, parallelization, orchestrator-worker, evaluator-optimizer, agent — each mapped to our node vocabulary, with the one question that separates the pairs people confuse.
How to build your own UI
Both shipped surfaces are ordinary HTTP clients, so a third one is supported rather than reverse-engineered. The committed OpenAPI document, the three SSE streams it structurally cannot express, the five calls a custom chat needs — and a complete client in forty lines.
How to let an LLM build it
The MCP layer, worked end to end: the client config JSON, a document that genuinely
validates, the compile_workflow response shape, and the trust boundary.
How to add a module
Find the registry for the thing you are adding, write the definition, register it. The engine does not change. A complete tool node, both halves, in about sixty lines.
How it works
The layering rule, the one-way compile seam, and the architecture non-negotiables — including why we compile instead of interpreting, and why there is no second runtime.
Why it works that way
Decision records for the choices that were argued out — the knowledge architecture, rejected frameworks, and the trade-offs each one accepted.
Beta: documentation links on this page point into the beta
repository, https://github.com/zulfeekar/openstategraph, which is
private today — every link above 404s for anyone who is not a
collaborator. They move to the stable home together with the code, and open up when it
does — python3 scripts/build_site.py --check says whether the links travel.
The ten questions people actually ask
Short answers, with the long version in the repository. If one of these is wrong or missing, that is a documentation bug — open an issue.
Is this just another workflow runtime?
No — and the distinction is the whole project. Tools like n8n, Dify, Langflow and
Flowise own their executor: your flow runs inside their engine or not at all.
OpenStateGraph is a compiler with a single target. It turns the canvas into
workflow.json, and workflow.json into a LangGraph
StateGraph. There is no engine of ours in the loop at runtime.
The practical consequence: checkpointing, time travel, interrupt(),
Send fan-out and streaming are LangGraph's, not reimplementations that
lag behind it.
Do I need the editor running in production?
No. The compiled graph is an ordinary Python object. Import it from a script, test it
with pytest, wrap it in your own FastAPI app, deploy it wherever Python
runs. The editor is an authoring tool, not a dependency of the thing you author.
Delete this repository and your workflow package still runs. That is the test we hold ourselves to: the compiler is not portable; the output is.
The three ways to consume it — fork/checkout, artifact, MCP — are written up with exact commands in docs/adoption.md.
Do I need OpenStateGraph installed to run my workflow?
Yes, and the honest answer has three parts — because people test this claim, and one slogan would be wrong in at least one of them.
workflow.json needs us. It is our format; nothing else reads it.
A package needs us too, because tools/,
functions/, middlewares/, skills/ and
knowledge/ are wired by our discovery conventions — and that wiring is
the difference between a workflow that answers and one that only looks like it does.
Compile the document by hand and the agent is drawn with three tools, bound to none,
and answers confidently from memory instead of from your database.
load_workflow exists because of exactly that failure, and it reports
whatever it could not resolve on .warnings.
The compiled graph does not need us. workflow.graph is a plain
LangGraph StateGraph — .stream(),
.astream_events(), .get_state(), interrupt and resume, your
own checkpointer — with nothing of ours in the call stack. So the dependency buys you
the format, the compiler and the package conventions; it is not a runtime tax, and
there is a full escape hatch with no proprietary object in the way.
Footprint, measured in a clean virtualenv rather than estimated: 36
distributions for the core, 38 with a provider — essentially all of them
LangChain's and LangGraph's own closure, which you would install anyway to write the
StateGraph by hand. The long version, including what we deliberately do
not own and when not to use us at all, is in
docs/what-is-this.md.
Can my own LLM generate workflows?
Yes — that is what the MCP layer is for, and the inversion is deliberate. Your
model composes; this server is the ground truth and the artifact factory. A client
calls get_node_vocabulary() for the exact node types, port ids and
binding rules, composes a document, then calls compile_workflow(). The
verdict is deterministic and calls no model of ours: an invalid document returns
findings and no artifacts, so the client iterates against compiler
evidence rather than its own confidence.
A valid one returns the workflow.json envelope to commit to
your repository, the compiled Mermaid topology, a run snippet and the package
layout. Clients like Claude render that Mermaid as a diagram locally, so asking for a
workflow in a chat window gives you a picture of the compiled
StateGraph — with the graph never leaving your machine.
The trust boundary holds: no publish tool, no delete tool, no credentials over the wire, writes validated server-side and always as drafts. A human clicks Publish. Worked example in docs/mcp.md; the honest limits — starting with no authentication layer — in docs/decisions/mcp-layer.md.
Which models can I use, and where do my keys live?
Anything LangChain's init_chat_model can resolve — Anthropic, OpenAI,
Google, Bedrock, Ollama cloud and the rest. A model is a string on a node, so
switching provider is an edit, not a migration.
Keys stay server-side, read from the backend's environment
(ANTHROPIC_API_KEY, OPENAI_API_KEY); with none set, the
backend defaults to Ollama cloud with no configuration. Keys are never baked into
workflow.json, so the document is safe to commit.
How do I add a node type or a tool?
You register it; you never edit the engine. Every extension point is a registry —
node types, executors, model providers, connection rules, validation rules, canvas
features, card bodies — so a new capability lands without touching core/.
A node type is one file (model, field schema, ports, executor) plus its registry
entry; a tool is a BaseTool subclass discovered by convention inside a
workflow's tools/ directory. You can also skip this repository
altogether: publish your own distribution declaring
[project.entry-points."openstategraph.tools"] and your tool registers in
every workflow the moment someone pip installs it — no fork, no merge to
carry. See docs/building-an-atom.md →
What is the “second brain”?
Per-workflow knowledge, built from your real sources and stored as markdown in the
package's knowledge/ directory. It is chunked deliberately: one index
line per topic is cheap enough to carry in every prompt, and the full document is
fetched only once an agent commits to that direction.
It is generated, then owned by you — the first keystroke claims a document, and a
stale badge tells you when the source behind it moved. In
chinook-assistant it is eleven docs, one per table, so the model knows
what InvoiceLine means before it writes a join.
What is the licence, and can I use it commercially?
MIT, for the whole project. Use it commercially, fork it, embed it in a product, keep your changes private — no contributor licence agreement, no open-core tier, no feature held back for a paid edition. Third-party notices are listed in THIRD_PARTY_NOTICES.md; the models you call keep their own terms.
How is my data handled?
Local-first. Workflows are files on your disk, runs execute on your machine, and there is no OpenStateGraph service to phone home to — the page you are reading makes no network requests either. The only traffic leaving your machine is the model call you configured.
One deliberate consequence: graph previews use LangGraph's draw_mermaid(),
which returns text, rather than draw_mermaid_png(), which would post your
graph to a third-party rendering API. We never send a user's graph to a stranger.
Why LangGraph, and why only LangGraph?
Because it is the one runtime whose primitives a canvas maps onto honestly: nodes and
conditional edges, subgraphs, typed shared state with reducers, Send
fan-out, durable checkpoints. Drawing a box on a canvas means something exact there.
And only LangGraph because a multi-runtime abstraction would be unbindable, not merely
leaky — no competing framework accepts a serialisable graph, so the interface would
have nothing to bind to. Portability is preserved where it is cheap instead:
workflow.json stays vendor-neutral, expressions are a JSON AST rather
than host-language lambdas, reducers are a named enum, and no LangGraph type name
leaks into the document.