Graph

In shortReference for StateGraph, CompiledGraph, Node, Edge, BaseAgent and RemoteToolConfig: signatures, parameters, returns, config keys and errors.

  • 14 min read
  • 11 sections
  • Updated
  • v0.10.0
  • Markdown

The graph module provides the core building blocks for constructing and executing agent workflows. StateGraph builds workflows, CompiledGraph runs them, and supporting classes like Node, Edge, BaseAgent, and RemoteToolConfig configure execution. Every 10xGraph application starts with a graph.

Import paths

Python
from tenxgraph.core.graph import (
    Agent, BaseAgent, CompiledGraph, Edge, Node, RemoteToolConfig, StateGraph, ToolNode
)
from tenxgraph.core.state import AgentState, Message, StreamChunk
from tenxgraph.utils import END, START, ResponseGranularity

Constants

The two sentinel names that mark where a graph starts and ends.

Name Value Description
START "__start__" Sentinel node name. Use as the source of the first edge to set the entry point.
END "__end__" Sentinel node name. Add an edge to END from any terminal node.

StateGraph[StateT]

The builder class. Construct a workflow by adding nodes and edges, then call compile() to get an executable CompiledGraph. For the concept, see State graph.

Constructor

The constructor takes an optional state, plus optional context manager, publisher, ID generator and DI container.

Python
from tenxgraph.core.graph import StateGraph
from tenxgraph.core.state import AgentState

graph = StateGraph()                          # default AgentState
graph = StateGraph(MyCustomState())           # custom state instance
graph = StateGraph(MyCustomState)             # custom AgentState subclass (instantiated automatically)

Parameters:

Parameter Type Default Description
state StateT | type[StateT] | None None Initial state instance, or an AgentState subclass to instantiate. Defaults to AgentState().
context_manager BaseContextManager | None None Cross-node state operation handler.
publisher BasePublisher | list[BasePublisher] | None None Event publisher for monitoring. A list is combined into one composite publisher.
id_generator BaseIDGenerator | None None ID generator for messages and threads. Uses DefaultIDGenerator() when None.
container InjectQ | None None Dependency injection container. Uses global singleton if None.

Methods

add_node

Registers a node. Pass a function, or a name plus a function, ToolNode or Agent.

Python
graph.add_node(func)                          # name inferred from function.__name__
graph.add_node("my_node", func)              # explicit name
graph.add_node("agent", agent_instance)      # Agent or ToolNode instance

Returns StateGraph for chaining. Raises ValueError if the arguments match none of the patterns below.

Calling pattern When to use
add_node(func) Simple function nodes where the function name is a good node name.
add_node("name", func) When you need a custom node name.
add_node("name", agent) When attaching an Agent or ToolNode instance.

add_edge

Adds a static edge between two nodes.

Python
graph.add_edge(START, "entry")   # also sets the entry point
graph.add_edge("entry", END)
graph.add_edge("node_a", "node_b")

The graph always follows this route. An edge from START also sets the entry point. Returns StateGraph.


add_conditional_edges

Routes from a node using a function evaluated at run time. The condition receives only the current state (not config) and must be side-effect free.

Python
def route(state: AgentState) -> str:
    # Return a node name (or END) directly
    if state.context and state.context[-1].text() == "done":
        return END
    return "process"

graph.add_conditional_edges("check", route)

# With a path map: the return value is looked up in the map
graph.add_conditional_edges(
    "classify",
    lambda state: "urgent" if "urgent" in state.context[-1].text() else "normal",
    path_map={
        "urgent": "urgent_handler",
        "normal": "normal_handler",
    },
)

Parameters:

Parameter Type Description
from_node str Source node name.
condition Callable Function receiving state and returning a routing key or node name.
path_map dict[str, str] | None Maps condition return values to node names. If None, the condition must return a node name directly. Returns StateGraph.

set_entry_point

Sets the first node to run.

Python
graph.set_entry_point("my_node")
# equivalent to graph.add_edge(START, "my_node")

override_node

Replaces an existing node with a new function, Agent, or ToolNode. Raises KeyError if the node does not exist. Useful for swapping production nodes with test doubles before compilation.

Python
graph.override_node("MAIN", test_agent)

compile

Validates the graph and returns an executable CompiledGraph.

Python
app: CompiledGraph = graph.compile()

app = graph.compile(
    checkpointer=my_checkpointer,
    store=my_store,
    media_store=my_media_store,
    interrupt_before=["review_node"],
    interrupt_after=["tool_node"],
    shutdown_timeout=30.0,
)

Parameters:

Parameter Type Default Description
checkpointer BaseCheckpointer | None None State persistence backend. Defaults to InMemoryCheckpointer.
store BaseStore | None None Long-term memory store.
media_store BaseMediaStore | None None Media/file storage backend.
interrupt_before list[str] | None None Node names to pause execution before.
interrupt_after list[str] | None None Node names to pause execution after.
callback_manager CallbackManager CallbackManager() Hooks for evaluation collectors and monitoring.
shutdown_timeout float 30.0 Seconds to wait for background tasks during graceful shutdown.

Raises: GraphError with GRAPH_002 if no entry point is set, GRAPH_003 if a node is orphaned (no edge touches it), or GRAPH_004 if an edge target or an interrupt node does not exist.


CompiledGraph[StateT]

The executable graph produced by StateGraph.compile(). Do not instantiate directly.

invoke

Runs the graph synchronously and returns the result.

Python
result = app.invoke(
    {"messages": [Message.text_message("Hello")]},
    config={"thread_id": "session-1", "user_id": "alice"},
    response_granularity=ResponseGranularity.LOW,
)

Blocks until the graph finishes. It uses asyncio.run() internally, so do not call it from an async context. Use ainvoke() instead.


ainvoke

The asynchronous form of invoke. It starts fresh or resumes an interrupted run.

Python
result = await app.ainvoke(
    {"messages": [Message.text_message("Hello")]},
    config={"thread_id": "session-1"},
    response_granularity=ResponseGranularity.FULL,
)

Parameters (both invoke and ainvoke):

Parameter Type Default Description
input_data dict[str, Any] required Input dict. Must contain "messages" for new runs.
config dict[str, Any] | None None Execution config. Keys listed in Config dictionary keys.
response_granularity ResponseGranularity LOW Controls how much is included in the response. See table below.

ResponseGranularity values:

Value Import What is returned
LOW from tenxgraph.utils import ResponseGranularity messages only
PARTIAL same import messages, context, summary
FULL same import messages, context, summary, full state object

Returns: dict with keys depending on granularity. A graph that contains a LiveAgent raises RuntimeError; drive it with arealtime(). For streaming and interrupts, see Stream a graph.


stream / astream

Run the graph and yield StreamChunk objects as it executes.

Python
# Sync
for chunk in app.stream({"messages": [msg]}, config={"thread_id": "t1"}):
    if chunk.event == "message":
        print(chunk.message.content)

# Async
async for chunk in app.astream({"messages": [msg]}, config={"thread_id": "t1"}):
    if chunk.event == "message":
        print(chunk.message.content)

StreamChunk fields:

Field Type Description
event StreamEvent One of MESSAGE, STATE, UPDATES, ERROR (string values "message", "state", "updates", "error").
message Message | None Populated for MESSAGE events.
state AgentState | None Populated for STATE events.
data dict | None Populated for UPDATES and ERROR events.
thread_id str | None Thread ID for this execution.
run_id str | None Run ID for this execution.
metadata dict | None Optional extra metadata.
timestamp float UNIX timestamp of chunk creation.

stop / astop

Request that a running thread stop.

Python
# Async: call from within an async route handler
resp = await app.astop(config={"thread_id": "session-1"})
# {"ok": True, "running": True}

# Sync
resp = app.stop(config={"thread_id": "session-1"})

Sets a stop flag on the running execution. The graph checks this flag between node transitions and halts cleanly. config must carry the thread_id of the run. Without a checkpointer it returns {"ok": False, "reason": "no-checkpointer"}.

Returns: dict with ok, running, and optional reason keys.


override_node (on CompiledGraph)

Python
compiled.override_node("MAIN", test_agent)

Same semantics as StateGraph.override_node() but on an already-compiled graph. Useful when testing a compiled production graph without rebuilding it.


Async context manager

Use async with on a compiled graph so resources are released on exit. CompiledGraph implements the async context manager protocol. Use async with to ensure aclose() runs automatically on exit, even when an exception is raised:

Python
async def main():
    graph = graph_builder.compile()
    async with graph:
        result = await graph.ainvoke({"messages": [msg]})

# aclose() has been called here, even if ainvoke raised

When your graph is built by an async factory function, await the factory first, then use async with:

Python
async def main():
    async with await build_and_compile_graph() as graph:
        result = await graph.ainvoke(input_data)

aclose

Releases the graph’s resources.

Python
await app.aclose()

It stops background tasks, closes the checkpointer, closes the publisher, and releases the store. Always call this when shutting down a long-lived application.

aclose() is idempotent: calling it a second time is a no-op and returns {"status": "already_closed"} rather than raising. This means calling aclose() explicitly inside an async with block, then having __aexit__ call it again, is safe.


generate_graph

Returns a dictionary describing the graph structure.

Python
graph_info = app.generate_graph()

Returns: dict with keys nodes (list of node objects with id and name), edges (list of edge objects with id, source, target), and info (metadata including node and edge counts, checkpointer and store presence, interrupt lists, context manager type, ID generator type, state type and state fields). Node and edge id values are random UUIDs generated on each call.


attach_remote_tools

Attaches client-executed tools to a ToolNode.

Python
from tenxgraph.core.graph import RemoteToolConfig

tools = [
    RemoteToolConfig(
        node="tool_node",
        name="read_clipboard",
        description="Read the current clipboard text",
        parameters={
            "type": "object",
            "properties": {},
            "required": [],
        },
    )
]
app.attach_remote_tools(tools)

# Or pass OpenAI-compatible schemas with a node name
openai_schemas = [{
    "type": "function",
    "function": {
        "name": "read_clipboard",
        "description": "Read the current clipboard text",
        "parameters": {"type": "object", "properties": {}, "required": []},
    },
}]
app.attach_remote_tools(openai_schemas, node_name="tool_node")

Remote tools run on the client, not the server. The graph advertises their schemas to the model and emits a RemoteToolCallBlock when one is called.

Parameters:

Parameter Type Description
tools list[dict or RemoteToolConfig] Tool configurations. Each can be a RemoteToolConfig with node, name, description, parameters, or an OpenAI function schema.
node_name str or None ToolNode name when passing OpenAI schemas. Omit if each tool carries its own node value.

Raises: GraphError (GRAPH_004) if the node does not exist, GraphError (GRAPH_005) if it is not a ToolNode, and ValueError for duplicate tool names or a tool whose node differs from node_name.


is_realtime

Reports whether the graph must be driven in realtime mode.

Python
if app.is_realtime():
    # arealtime(input_queue, config=None, state=None) yields realtime events
    async for event in app.arealtime(input_queue):
        print(event)
else:
    result = app.invoke({"messages": [msg]})

Returns True if the graph contains at least one LiveAgent, meaning it must be driven via arealtime() or realtime() rather than invoke() or stream(). See Use realtime audio.

Returns: bool


Node

Represents an executable unit within a graph. It wraps a function, ToolNode, or Agent that processes state and returns output.

Constructor:

Python
from tenxgraph.core.graph import Node

node = Node("processor", my_function)
Parameter Type Description
name str Unique identifier for the node within the graph.
func Callable, ToolNode, or BaseAgent The function, ToolNode, or Agent to execute. Functions must accept at least state and config as positional arguments.

Nodes are typically added to a graph via StateGraph.add_node() rather than instantiated directly.


Edge

Represents a connection between two nodes in the graph, defining routing logic and execution flow.

Constructor:

Python
from tenxgraph.core.graph import Edge

# Static edge
edge = Edge("process", "approval")

# Conditional edge: the condition receives only the state
def needs_approval(state):
    return bool(state.context) and "approve" in state.context[-1].text()

conditional_edge = Edge("process", "approval", condition=needs_approval)
Parameter Type Description
from_node str Name of the source node.
to_node str Name of the destination node or special constant (END).
condition Callable or None Optional function receiving state that returns a node name, a routing key or a boolean. If None, the edge is static and always followed.

Edges are typically added to a graph via StateGraph.add_edge() or StateGraph.add_conditional_edges() rather than instantiated directly.


BaseAgent

Abstract base class for all agents (production and test). Subclasses must implement execute() and _call_llm(). Agent is the concrete production subclass; its own constructor accepts more options (see Agent).

Constructor:

Python
from tenxgraph.core.graph import Agent

# Agent is a concrete subclass of BaseAgent
agent = Agent(
    model="gpt-4o",
    provider="openai",
    system_prompt=[{"role": "system", "content": "You are helpful."}],
)

BaseAgent parameters:

Parameter Type Default Description
model str required LLM model identifier (for example "gpt-4o").
provider str or None None Explicit provider override. Inferred from model if not set.
system_prompt list[dict] or None None System prompt as a list of message dicts.
tool_node str, ToolNode, or None None ToolNode instance or name of an existing graph node whose func is a ToolNode.
extra_messages list[Message] or None None Additional messages to prepend to the context.
client Any None Escape hatch: custom LLM client.
base_url str or None None For OpenAI-compatible APIs (Ollama, vLLM).
trim_context bool False Whether to trim long contexts.
tools_tags set[str] or None None Filter tools by tags.
**llm_kwargs Any Extra provider-specific parameters.

Methods:

Method Description
async execute(state, config) Main execution logic. Must be implemented by subclasses.
get_tool_node() Returns the agent’s internal ToolNode, or None if not configured (the base class always returns None; Agent overrides it).
async __call__(state, config) Makes the agent callable like a function. Delegates to execute().

RemoteToolConfig

Validated Pydantic model for a tool executed by the client rather than the server. Unknown fields are rejected, so typos fail at startup.

Constructor:

Python
from tenxgraph.core.graph import RemoteToolConfig

config = RemoteToolConfig(
    node="tool_node",
    name="read_clipboard",
    description="Read the current clipboard text",
    parameters={
        "type": "object",
        "properties": {
            "full": {
                "type": "boolean",
                "description": "Return full history if true",
            }
        },
        "required": ["full"],
    },
)
Parameter Type Description
node (alias node_name) str ToolNode name where this tool will be registered.
name str Tool name (1+ characters, non-blank after stripping).
description str Tool description (1+ characters, non-blank after stripping).
parameters dict JSON Schema for tool parameters. Must have type: "object", a properties dict and a required list of strings; missing keys are filled in. Defaults to an empty object schema.

Methods:

Method Description
to_tool_schema() Returns the OpenAI-compatible function schema consumed by ToolNode.

Config dictionary keys

The config dict passed to invoke, ainvoke, stream, astream and stop accepts these keys. Missing identifiers are generated for you.

Key Type Auto-generated if missing Description
thread_id str | int Yes (UUID or from ID generator) Identifies the conversation thread for checkpointing.
user_id str Yes ("anonymous") User identifier. Override in production.
run_id str | int Yes Unique run identifier.
recursion_limit int No (framework default: 25) Maximum node transitions before GraphRecursionError.
timestamp str Yes ISO 8601 timestamp of the run.
node_timeout float | None No (framework default: 900.0 seconds) Deadline for a single node execution.
tool_timeout float | None No (framework default: 300.0 seconds) Deadline for a single tool call inside a ToolNode.
durable_checkpoint_every_step bool No (default True) Persist state and new messages after each completed step. See Set up checkpointing.

Execution deadlines

Every node and tool call has a deadline, so a hung call cannot block a run forever.

Nothing else bounds a node or a tool. The provider SDK’s own timeout does not cover custom tools, MCP calls, or condition functions, so without a deadline a hung call blocks the run forever: the loop never advances a step, the recursion limit never trips, and the between-nodes stop check is never reached.

Both defaults are deliberately generous. They are a backstop against hangs, not a latency budget. The node default (900s) sits above the LLM client timeout (600s) so a slow LLM call fails with its own error rather than being masked by the node deadline.

Python
result = app.invoke(
    input_data,
    config={
        "thread_id": "t-1",
        "node_timeout": 120.0,   # tighten the node deadline
        "tool_timeout": 30.0,    # tighten the tool deadline
    },
)

# Disable a deadline entirely: pass None, or any non-positive number
config = {"thread_id": "t-1", "node_timeout": None}

An absent key uses the framework default. An explicit None disables the deadline, which is different from the key being absent. A non-positive value (0) also disables it. A value that cannot be parsed as a float logs a warning and falls back to the default.

When a deadline expires the node task is cancelled and a NodeTimeoutError is raised, which the execution loop persists and reports as a normal node error.


Node function signature

Node functions receive the current state and config as their first two positional arguments. Additional keyword arguments are resolved from the dependency injection container (see Dependency injection):

Python
from tenxgraph.core.state import AgentState, Message
from tenxgraph.storage.store import BaseStore

def my_node(state: AgentState, config: dict, store: BaseStore) -> list:
    # Return a list of Message objects to append to state.context
    return [Message.text_message("response", role="assistant")]

Alternatively, functions can return an updated state dict:

Python
def my_node(state: AgentState, config: dict) -> dict:
    return {"context_summary": "Updated summary"}

Common errors

Error Cause Fix
GraphError GRAPH_002 compile() called with no entry point. Call set_entry_point() or add_edge(START, "node").
GraphError GRAPH_003 A node has no edge touching it. Connect the node or remove it.
GraphError GRAPH_004 An edge target, an interrupt node, or an attach_remote_tools node does not exist. Check node names against graph.nodes.keys().
GraphError GRAPH_005 attach_remote_tools targeted a node that is not a ToolNode. Pass the name of a ToolNode node.
GraphError GRAPH_ROUTING_001 A conditional edge function raised while routing. Fix the condition function; it receives only state.
GraphRecursionError Execution exceeded recursion_limit. Increase recursion_limit in config or fix a cycle in the graph.
RuntimeError in async context invoke() called inside a running event loop. Use ainvoke() instead.
RuntimeError on a realtime graph invoke or stream called on a graph with a LiveAgent. Use arealtime().
NodeTimeoutError A node or tool exceeded node_timeout / tool_timeout. Raise the relevant deadline in config, or fix the hanging call.
Last updated for v0.10.0Edit this page on GitHubReport an issue