Skip to content

The Context object

Every node is called as (state, ctx). The state is the data; the ctx is a small per-node handle that carries the node's identity and gives it two abilities: surface events, and suspend for a human.

Identity

async def node(state, ctx):
    ctx.node    # this node's name
    ctx.thread  # the run's thread id
    ctx.step    # the current super-step number

These are useful for logging and for keying external work by run.

ctx.emit — surface an event

ctx.emit(event) pushes an event to two places at once: the run's live stream (so app.stream sees it) and the telemetry hooks (so on_event fires). Use it to stream a model's tokens or to record tool calls and gate decisions.

from agentflow.events import TextChunk

async def chat_node(state, ctx):
    text = []
    async for ev in llm.chat(state["messages"]):
        if isinstance(ev, TextChunk):
            ctx.emit(ev)          # -> stream + telemetry
            text.append(ev.text)
    return {"reply": "".join(text)}

emit accepts any backend event (TextChunk, ToolCall, ToolResult, …) and the run-level GateEvent. It is synchronous and best-effort: the telemetry delivery is scheduled fire-and-forget, so its ordering relative to node-start/end callbacks isn't guaranteed, and calling it outside a running loop is simply skipped. Rely on it for content and tracing, not for lifecycle accounting.

ctx.interrupt — suspend for a human

await ctx.interrupt(payload) pauses the run for a person. On a fresh run it raises internally; the runtime catches it, writes an interrupted checkpoint carrying your payload, and stops. When the run is later resumed with a value, the same interrupt call returns that value instead of raising.

async def confirm(state, ctx):
    answer = await ctx.interrupt({"question": "Ship it?", "diff": state["diff"]})
    return {"approved": answer == "yes"}

That's the whole human-in-the-loop mechanism — there's no special node type. Because the frontier is persisted, it works across process restarts. See human-in-the-loop for the resume side and quality gates for a structured way to build on it.

How helpers reach the context

The runtime sets the current node's context in a contextvar around each node call. That's how an Interactive permission policy — running inside a backend the node is consuming — can call ctx.interrupt without you threading ctx through every layer. A node and any backend generator it iterates share one task, and contextvars are per-task, so the right context is always in scope.

Next: running a graph.