Skip to content

Command (dynamic routing)

A Command is a node return value that does two things at once: it merges a state update and names the node the graph should run next. It is Teff's LangGraph-style dynamic edge — a node picks its successor at runtime instead of being wired to fixed routes.

from teff.node import Command


class AdminGate(Node):
    async def execute(self, ctx, state):
        if state.get("role") == "admin":
            return Command(update={"allowed": True}, goto="admin_tools")
        return Command(update={"allowed": False}, goto="denied")

Why it exists

Normally a node returns a plain dict of state updates and routing is decided by the graph's edges (branch / loop / string conditions on step(...)). Command covers the cases edges can't express cleanly:

  • the next node depends on the result of the node just executed;
  • you want to skip intermediate nodes and jump to a specific node;
  • you want to terminate the run from the middle of the graph.

The three forms

Returned value Effect
Command(update={...}, goto="node_id") Merge update, then route to node_id (any node, even if no edge leads there).
Command(update={...}) Merge update only — routing follows the normal outgoing edges.
Command(goto=Command.STOP) End the run immediately; the node's enclosing iteration stops.
Command() A no-op: the node finishes without changing state or routing.

goto

goto accepts any node id in the graph — it does not need to be a declared edge target. This is what makes it dynamic:

async def route(self, ctx, state):
    if state.get("diag") == "fatal":
        return Command(goto="incident")
    return {"diag": state.get("diag")}  # normal edge routing

Routing to an unknown id raises WorkflowError. Command.STOP is a sentinel that terminates the run (equivalent to a node with no outgoing edge, but explicit).

update

update is merged into state exactly like a plain dict return — per-key reducers still apply. A Command with only update and no goto behaves like returning the dict, so you can opt into a small, readable node that both writes and routes.

Sub-workflows

Inside a Parallel/Map branch only the update part is applied — the goto is a top-level concern and is ignored there, so a branch can update state but not hijack the parent's control flow.

Plain functions can return it too

Any node accepted by flow.step — including a plain (ctx, state) -> dict function — may return a Command:

flow.step(lambda ctx, s: Command(update={"seen": True}, goto=Command.STOP))

Function nodes that return neither a dict nor a Command raise a TypeError.

Example

See examples/command_routing for the CLI/offline walkthrough (Flow + raw Graph), and the production-style FastAPI app examples/applications/fraud_gate for a payment gate whose router uses Command to auto-approve, send mid-risk payments to a human (a durable Interrupt) or deny-and-stop — all driven by an LLM risk score.

python examples/command_routing/flow.py    # Flow API
python examples/command_routing/graph.py   # raw Graph API

STOP doesn't mean error

Ending a run with Command.STOP is a normal, successful termination — the final state is returned as usual. It differs from raising an error or the Interrupt (which pauses and can be resumed).