Skip to main content
langgraph-declarative

Topology in YAML. Logic in Python. One line to compile.

CI Python License PyPI Downloads

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: and subgraph: read any YAML file the process can reach, including ../ and absolute paths.
  • Callable selection — function: and path: 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 call invoke(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 for llm: 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)

Source distribution for langgraph-declarative 0.3.0
File Size Uploaded
langgraph_declarative-0.3.0.tar.gz 68.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langgraph-declarative 0.3.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page