Topology in YAML. Logic in Python. One line to compile.
Quickstart · Features · YAML reference · Human-in-the-loop · Examples · Roadmap
Describe a LangGraph workflow's structure in YAML, keep the behaviour in Python, and compile the two into a standard CompiledStateGraph. Nothing about how you run, stream, or deploy the graph changes.
Install
pip install langgraph-declarative # uv add langgraph-declarative
pip install "langgraph-declarative[anthropic]" # optional: llm: support ([openai] too)
Security & trust
Workflow definitions are trusted input — treat them like code. YAML is parsed with
yaml.safe_load, so a file cannot construct arbitrary Python objects, but a definition
still decides what runs and what is read:
- Module imports — a
tools:entry such as"module.path:attr"imports that module (running its top-level code) and binds whatever attribute it names. - Filesystem reach —
imports:andsubgraph:read any YAML file the process can reach, including../and absolute paths. - Callable selection —
function:andpath:may name any callable in your registry.
That is fine for files in your repository or a database only your team writes. Do not build graphs from definitions supplied by end users or other untrusted parties. A restricted mode for that case is a future, demand-gated item (roadmap M08).
Quickstart
1. Register your functions.
from langgraph_declarative import Registry, build_graph
registry = Registry()
@registry.node("greet")
def greet(state):
return {"messages": [{"role": "assistant", "content": "Hello! How can I help?"}]}
@registry.node("respond")
def respond(state):
return {"messages": [{"role": "assistant", "content": "Goodbye!"}]}
2. Declare the graph.
# workflow.yaml
nodes:
- name: "greeter"
function: "greet"
- name: "responder"
function: "respond"
edges:
- source: "START"
target: "greeter"
- source: "greeter"
target: "responder"
- source: "responder"
target: "END"
3. Build and run.
graph = build_graph("workflow.yaml", registry)
result = graph.invoke({"messages": [{"role": "user", "content": "Hi there"}]})
That's the whole surface area. build_graph() validates the YAML with Pydantic, cross-checks every function: and path: against the registry, and hands you a compiled LangGraph.
How it fits together
flowchart LR
Y["workflow.yaml<br/><i>or a DB row</i>"] --> V["Schema validation<br/><i>Pydantic</i>"]
R["@registry.node<br/>@registry.router<br/>@registry.tool"] --> V
V --> B["GraphBuilder"]
B --> G["CompiledStateGraph<br/><i>.invoke() · .stream() · langgraph dev</i>"]
Three pieces: a registry of Python functions, a definition of the topology, and a builder that joins them. Validation happens before compilation, so a typo in the YAML gives you Unknown node function 'classfy'. Did you mean 'classify'? — not a stack trace at runtime.
Features
| Feature | YAML | Since | Example |
|---|---|---|---|
| Simple & parallel edges | target: "node" / target: [a, b] |
0.1.0 | quickstart, fan_out |
| Conditional routing | path: + targets: |
0.1.0 | conditional_routing |
Dynamic fan-out (Send) |
path: without targets |
0.1.0 | dynamic_routing |
| Custom Python state class | build_graph(..., state_class=...) |
0.1.0 | custom_state |
| State declared in YAML | state: with types & reducers |
0.2.0 | declared_state |
| Match routing (no router fn) | match: + targets: |
0.2.0 | match_routing |
| Mermaid diagrams | draw_mermaid() |
0.2.0 | visualization |
| IDE autocomplete & validation | workflow.schema.json |
0.2.0 | visualization |
| Subgraph composition | subgraph: "child.yaml" |
0.2.0 | subgraph |
| Cross-file node imports | imports: |
0.2.0 | cross_file_imports |
| LLM config & tool binding | llm: + tools: |
0.2.0 | llm_and_tools |
| Database-stored workflows | SQLiteLoader, build_graph_from_db() |
0.2.0 | db_workflow |
| LangGraph project template | — | 0.2.0 | template/ |
| Human-in-the-loop | build_graph(..., checkpointer=...), destinations:, interrupt_before: |
0.3.0 | human_in_the_loop |
YAML reference
Only nodes is required. The simplest workflow is a list of nodes and edges.
Unknown keys are rejected with the closest valid key suggested (from 0.3.0), so a
typo such as interupt_before: fails loudly instead of silently doing nothing. Use
description: on the graph, a node or an edge for free-text documentation.
Full schema — state, llm, imports, nodes, edges
description: "Support triage" # optional free text — also on nodes and edges
state: # declare the state schema (default: MessagesState)
- name: "category"
type: "str" # str | int | float | bool | list | dict | list[str] | list[dict]
- name: "notes"
type: "list[str]"
reducer: "append" # append | add_messages | replace (default)
# default: [...] # deprecated (0.3.0): introspection-only, never
# applied at runtime — initialise in the graph input
llm: # graph-level LLM default for opt-in nodes
provider: "anthropic" # anthropic | openai
model: "claude-opus-4-8"
imports: # merge node declarations from other files
- file: "shared_nodes.yaml" # relative to this file
nodes: ["error_handler"] # omit to import all nodes
interrupt_before: ["approval"] # pause before these nodes run (needs a checkpointer)
interrupt_after: [] # pause after these nodes run
nodes:
- name: "classifier"
function: "classify" # registered via @registry.node()
description: "Tags the request" # optional free text
- name: "research"
subgraph: "child.yaml" # embed another workflow, relative to the declaring file
- name: "agent"
function: "agent_fn" # function must accept an `llm` parameter to opt in
llm: { model: "claude-haiku-4-5" } # node-level override, merged over graph llm
tools: ["get_weather"] # @registry.tool() names or "module.path:attr"
- name: "approval"
function: "approval_fn"
destinations: ["execute", "END"] # diagram metadata for Command(goto=) — not enforced
edges:
- source: "START" # START, END, or a node name
target: "classifier" # simple edge
- source: "loader"
target: ["a", "b"] # parallel fan-out
- source: "classifier"
path: "router_name" # @registry.router(); omit targets for Send routing
targets: { key: "node" }
- source: "classifier"
match: "category" # route on a state field's value — no Python router
targets:
billing: "billing_agent"
default: "fallback" # reserved fallback key
Human-in-the-loop
Pause a graph for human approval, then resume it. Pass a checkpointer — interrupt()
pauses by persisting state, so without one a pause cannot be resumed.
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END
from langgraph.types import Command, interrupt
@registry.node("approval")
def approval(state):
decision = interrupt({"question": "Approve this write?"})
if decision == "accept":
return Command(goto="execute")
return Command(goto=END) # LangGraph's END constant — "END" is YAML-only
graph = build_graph("workflow.yaml", registry, checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "run-1"}}
result = graph.invoke({"request": "write"}, config) # pauses: __interrupt__ in result
graph.invoke(Command(resume="accept"), config) # resumes and executes
Give the approval node a destinations: list in YAML. It routes itself with
Command(goto=...) and so has no static outgoing edges — without destinations the
Mermaid diagram invents a wrong approval --> END edge:
nodes:
- name: "approval"
function: "approval"
destinations: ["execute", "END"]
destinations describes the graph; it does not restrict it. A node that returns
Command(goto=...) to a target missing from the list still goes there at runtime.
For a pause that needs no Python at all, declare it in YAML instead:
interrupt_before: ["approval"]
What the host owns
The library gives you pause and resume primitives, not an approval system. Your application is responsible for:
- Thread IDs — who owns a
thread_id, and that one user cannot resume another user's run. Anyone who can callinvoke(Command(resume=...), config)with a thread ID can answer its pause. - Resume authorization — checking that the caller may approve this action before passing their decision to the graph.
- Decision validation — the resume value reaches your node unchecked; validate it
(the example treats anything but
"accept"as a rejection). - Stale and repeated decisions — what happens when a decision arrives late, twice, or after the run moved on.
- Audit — recording who decided what, and when.
interrupt_before: / interrupt_after: create a pause, not an authorization check: a
static pause resumes on any invoke(None, config) for that thread.
See human_in_the_loop for a runnable approval gate.
API
| Function / Class | Description |
|---|---|
Registry() |
Holds node, router, and tool functions in separate namespaces |
@registry.node("name") |
Register a node function (transforms state) |
@registry.router("name") |
Register a router (returns a routing key or a list of Send) |
@registry.tool("name") |
Register a tool for tools: binding |
build_graph(path, registry, state_class=None, *, checkpointer=None, store=None) |
YAML file → compiled CompiledStateGraph |
build_graph_from_db(source, registry, db_path, state_class=None, *, checkpointer=None, store=None, base_dir=None) |
DB-stored definition → compiled graph ("flow" or "flow@2"). base_dir is required if the definition uses relative imports: / subgraph: paths |
draw_mermaid(path, registry, state_class=None, output_path=None) |
Compile and render a Mermaid diagram (.md → fenced block) |
export_json_schema(output_path=None) |
Emit the JSON Schema for workflow YAML files |
GraphBuilder(registry, state_class=None, *, checkpointer=None, store=None) |
Power-user class behind build_graph(); .build(config, *, base_dir=None) compiles an in-memory GraphConfig |
SQLiteLoader(db_path) |
Save/load versioned definitions; implements the pluggable Loader protocol |
Documentation
| Document | Contents |
|---|---|
| examples/ | 13 runnable examples, one per feature, organized by milestone |
| template/ | Starter project for langgraph dev using the declarative pattern |
| docs/04-plan/ROADMAP.md | What shipped per milestone, and unvalidated future ideas |
| CHANGELOG.md | Release history |
| DECISIONS.md | Why the API and the scope look the way they do |
| docs/02-requirements/REQUIREMENTS.md | Problem, users, success criteria |
| docs/03-design/DESIGN.md | Architecture, module responsibilities, trade-offs |
Project layout
src/langgraph_declarative/ # library (registry, schema, builder, state/llm factories, loaders)
schema/ # generated JSON Schema for workflow YAML
examples/ # runnable examples — one folder per feature
template/ # LangGraph starter template
tests/ # pytest suite + YAML fixtures
docs/ # requirements, design, roadmap, reviews
Requirements
- Python 3.10+
- LangGraph ≥ 1.0 · PyYAML ≥ 6.0.2 · Pydantic ≥ 2.8.2 (0.2.0 declared LangGraph ≥ 0.2)
- Optional:
[anthropic]/[openai]extras forllm:support
Contributing
Issues and pull requests welcome. Run the suite with uv run --extra dev pytest before opening a PR.
License
MIT. Community project — not affiliated with or endorsed by LangChain. Built on top of LangGraph.
Metadata
Release files for langgraph-declarative 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langgraph_declarative-0.3.0.tar.gz | 68.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langgraph_declarative-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 98.9 kB
Release files / langgraph_declarative-0.3.0.tar.gz
| Download URL | langgraph_declarative-0.3.0.tar.gz |
|---|---|
| Size | 68.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
263bd1c4d31c6d15f053ff8ead3d3554d0ff2e08a7583b0a05e137538cb74021
|
|
BLAKE2b-256 checksum How to use checksums |
0fa5f19d704f98e93a9f79018b28bb568d1f4888d6f535ced62119d0631b052a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / langgraph_declarative-0.3.0-py3-none-any.whl
| Download URL | langgraph_declarative-0.3.0-py3-none-any.whl |
|---|---|
| Size | 30.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1c12ea97f7a8570dd0937db51e263b1f56fa0dc0427ed7f3873726e281b85e78
|
|
BLAKE2b-256 checksum How to use checksums |
03545c160caaec9c59b2dd7cdea8bcdb2b2c871dead55e77c62965ad2ba45c2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log