Skip to content

Graph & Workflows

Neurosurfer has a graph layer for multi-step pipelines:

  • neurosurfer.graph.engine — a standalone DAG engine (Graph, GraphNode, GraphExecutor). Think of it as the framework's LangGraph analog.
  • neurosurfer.graph.workflow — a persisted, versioned Workflow package layered on the engine: save a graph to disk, register it, and run it later.

The Architect builds these graphs for you from plain English — these pages cover building and running them directly.

The pages

Page Answers
Node kinds The eleven kinds, their classes, and what each one needs.
Control flow Branching, looping, fanning out, recovering from errors.
Authoring in YAML The graph.yaml format, key by key.
Python in a graph The functions: sidecar, and what your callables receive.
State & secrets What a node can see — and what it must never see.
Validation What is checked, when, and what a failure means.
Workflow packages Persisting, registering, and running a graph later.
Building in Python The GraphBuilder fluent API.

Two ways to write a graph

Python, where each kind is a class:

from neurosurfer.graph import Graph, BaseNode

graph = Graph(name="pipeline", nodes=[BaseNode(id="writer", instructions="…")])

YAML, which is what a package stores on disk:

name: pipeline
nodes:
  - id: writer
    kind: base
    instructions: 

Same Graph, same validation, no second code path. See Authoring in YAML.

Build a graph

A Graph is a set of GraphNodes. Each node has an id, a kind, instructions, and optional depends_on edges.

What a node receives is what its own text names, plus the outputs of its depends_on. Nothing ambient: the graph's inputs are not appended to every node's prompt as a block, so a step that needs one interpolates it by name.

from neurosurfer.graph import Graph, GraphNode

researcher = GraphNode(
    id="researcher",
    kind="base",
    description="Fact-finding node.",
    goal="Research {topic} and produce exactly 5 key bullet points.",   # names its input
)

writer = GraphNode(
    id="writer",
    kind="base",
    description="Turns research notes into prose.",
    goal="Write a clear, 2-paragraph explanation from the research notes above.",
    depends_on=["researcher"],     # names nothing; the edge carries the notes
)

graph = Graph(
    name="content_pipeline",
    description="Research a topic, then explain it.",
    nodes=[researcher, writer],
    inputs=[{"name": "topic", "type": "string"}],
)

A declared input that no step names is reported by the validator, because the run would otherwise go green while the model answers as though it had been passed nothing.

Upgrading an existing workflow?

This scoping rule changed, and a workflow written against the older behaviour still validates and still runs — it just answers as though handed nothing. See Upgrading.

The kinds, briefly

Each has a class; the kind= string is the same node by another name.

  • BaseNode — one bounded LLM step. Reasons; cannot act repeatedly.
  • ReactNode — a multi-step tool-using node. Reasons and acts.
  • ToolNode — one registered tool, called directly. Acts; cannot reason.
  • FunctionNode / PythonNodedeterministic Python by import path.
  • RouterNode, LoopNode, MapNode, SubgraphNodecontrol flow.
  • InputNode, OutputNode — the edges of a run.

Full field reference: Node kinds.

Run a graph

GraphExecutor runs the DAG on a provider, resolving dependencies and passing outputs downstream:

from neurosurfer.graph import GraphExecutor

executor = GraphExecutor(graph, provider=provider)
result = executor.run({"topic": "how attention works in Transformers"})

print(result.execution_summary())
print("succeeded:", result.succeeded)
print("errors:", result.errors or "none")

The GraphExecutionResult exposes execution_summary(), succeeded, errors, and per-node output.

Validation runs first. A graph that cannot run is refused before a model is called, rather than partway through — see Validation.

Persist it

A graph saved as a versioned package can be registered and run anywhere:

from neurosurfer.graph.workflow import load_package, WorkflowRunner

pkg = load_package(pkg_dir)
result = WorkflowRunner(provider, cwd=repo_root).run(pkg, inputs={"topic": "gradient descent"})

See Workflow packages.

Don't want to hand-build graphs?

The Architect designs and builds a Workflow package from a plain-English description — then you run it exactly as above.