MIT licensed · compiles to LangGraph

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.

Why it matters

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.

ApproachWho executes your flowWhere 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.

terminal
$ 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.

What this is, in full →

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.

How it works

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.

canvas · what you draw
Questioninput.text Routerroute.classifier SQL Agentagent.llm Researchagent.llm Graderroute.grader Approvalhuman.approval revision loop
run.py · what you ship
# 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.

What it achieves

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.

The MCP layer, with a worked example →

The example in the repo

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.

The one you see first

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.

route.classifierroute.grader agent.llmweb tools
The data branch, in the same 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.

agent.llmsql tool route.graderinput.markdown

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.

Twenty-three more ship inside the wheel. Every pattern the node catalogue can express — a straight line, three kinds of loop, a mount three documents deep, a two-provider flagship — each with the diagram its compiler produced, what that drawing became in LangGraph, and the question it was actually asked. See the gallery →
Quickstart

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.

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

    terminal
    $ uv tool install "openstategraph[server,ollama]"
    $ openstategraph serve --open   # prints the URLs it landed on

    No --port takes 8000, or the next free port if 8000 is busy; --port N means exactly N and says so if it is taken; --port 0 lets the OS choose. It binds 127.0.0.1 by 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.json in it, and exit codes are fixed (0 ok, 1 failure, 2 usage, 3 a missing extra), so validate works as a CI gate.

    terminal
    $ 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 yet
    This 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. Until 0.3.0rc18 it named three: the build lived on TestPyPI, its dependencies on PyPI, and uv had to be told it could mix them. Those are gone. The exact version stays until a final release exists, because pip and uv skip pre-releases in an unpinned requirement — and everything below is unchanged either way.
  2. Clone the repository

    Everything — editor, runtime, example workflows and their sample database — is in this one tree.

    terminal
    $ git clone https://github.com/zulfeekar/openstategraph.git
    $ cd openstategraph
    The repository is private right now. This clone, and every github.com/zulfeekar/openstategraph link 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.
  3. Start both stacks

    One entry point. ./start dev runs Vite and uvicorn with hot reload; plain ./start builds the production Docker stack instead and serves everything from port 8000.

    terminal
    $ npm install
    $ ./start dev        # editor :5273 · runtime :8000 · ./start stop
    Two 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.
  4. Open the editor

    http://localhost:5273 opens 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.

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

    terminal · or run the same graph without the editor
    $ 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_query leads 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.
  6. Open the chat

    http://localhost:8000/chat is 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.

Using a workflow

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.

Developer

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.

End user

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.

Publishing is one deliberate human action. Saving writes 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.
How to collaborate

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.json and AGENTS.md required; 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.

Documentation

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.

docs/what-is-this.md →

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.

docs/getting-started.md →

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.

docs/adoption.md →

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.

docs/stability.md →

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.

docs/patterns.md →

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.

docs/api.md →

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.

docs/mcp.md →

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.

docs/building-an-atom.md →

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.

CLAUDE.md →

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.

docs/decisions →

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.

FAQ

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.