skip to content

How does a LangGraph node emit custom progress updates to the stream?

level: middleimportance: should knowfreq 42%

answer

  1. a side channel out of node code
  2. not state, not tokens
  3. one dedicated stream mode consumes it
  4. obtained from config or injected as a parameter
  5. ephemeral: never replayed on resume

basics

~10 s

Call get_stream_writer() from langgraph.config inside the node and invoke it with any serialisable payload, then consume those payloads with stream_mode="custom". This streams progress that is neither part of graph state nor model tokens.

solid answer

~40 s

State updates only appear at superstep boundaries and token streaming only covers chat-model output, so a node doing a long non-LLM job — a paginated API crawl, a file conversion — is silent while it works. The custom stream closes that gap. Inside the node, `writer = get_stream_writer()` (from `langgraph.config`) returns a callable; every `writer(payload)` immediately emits that payload to consumers reading `stream_mode="custom"`. The payload is arbitrary and does **not** touch graph state, so it is not checkpointed and not replayed. An alternative form takes the writer as a typed `StreamWriter` parameter in the node signature, which is what you must use for async nodes on Python 3.10, where the implicit context lookup is unavailable. In practice you combine it: `stream_mode=["updates", "custom"]`, mapping custom payloads to a progress bar.

code

python · 10 lines
python
from langgraph.config import get_stream_writer


def crawl(state: dict) -> dict:
    writer = get_stream_writer()
    pages = []
    for i in range(1, 5):
        writer({"type": "progress", "stage": "crawl", "done": i, "total": 4})
        pages.append(f"page-{i}")
    return {"pages": pages}

go deeper

for a junior

Know that a node can push its own progress messages to the stream by getting a writer and calling it, and that a dedicated stream mode delivers them.

for a middle

Explain why the other modes leave a gap during long non-LLM work, and be precise that custom payloads bypass state entirely — no reducer, no checkpoint, no final result.

for a senior

Show operational judgment: stable payload schemas, throttling in hot loops, never making custom events load-bearing, and the async-on-older-Python case where the writer must be injected.

for a principal

Own the boundary between ephemeral progress telemetry and durable run history — what belongs on the socket, what belongs in state, and what belongs in the tracing backend that survives a dropped connection.

## The gap it fills LangGraph's other streaming modes are tied to things the runtime already knows about: state after a step, the delta a node returned, tokens from a chat model. None of them says anything while a node body is halfway through work that is not a model call. A node that pages through 400 API results, or transcodes a file, or runs a slow SQL query, emits exactly nothing until it returns. The custom stream is the escape hatch: an explicit side channel from node code to the stream consumer. ## Two ways to get the writer **Implicit lookup.** `from langgraph.config import get_stream_writer`, then `writer = get_stream_writer()` inside the node. The runtime resolves the writer for the current execution from context, so the node signature stays clean. **Explicit parameter.** Declare the node as taking a second argument annotated `StreamWriter` (from `langgraph.types`) and the runtime injects it. This is the form to use for async node bodies on Python 3.10, where context propagation into async calls is not automatic and the implicit lookup will not find the writer. Outside a graph run — a unit test that calls the node function directly, say — the implicit lookup yields a no-op writer rather than raising, so custom writes silently disappear. That is convenient for testability and confusing during debugging; if your custom payloads never show up, check first that the code really is running inside a graph invocation. ## Payload discipline The payload is whatever you pass. Because it crosses a wire, treat it as a public event, not a debug print: - Give it a stable shape — `{"type": "progress", "stage": "fetch", "done": 40, "total": 400}` — so consumers can switch on `type` instead of pattern-matching prose. - Keep it small and serialisable. Writing a whole retrieved document through the custom channel re-creates the bandwidth problem the delta mode exists to avoid. - Do not put anything the user should not see in it. Custom writes bypass any projection you apply to state. ## What it is not **Not state.** The payload is not merged into any channel, has no reducer, is not persisted by a checkpointer, and is not part of the final result. If downstream nodes need the value, return it from the node as a normal state update as well. **Not replayed.** Because it is not persisted, resuming a run from a checkpoint does not re-emit earlier custom payloads. A UI that reconstructs its view from a resumed run must therefore be able to start from whatever state it can read, not from a complete history of custom events. This is the single most important consequence to state in an interview: custom events are ephemeral, best-effort telemetry for a live connection. **Not ordered against tokens by any guarantee you should lean on.** When several modes are combined the consumer sees an interleaved stream; treat relative ordering between a custom payload and a token chunk as incidental. ## Consuming it `stream_mode="custom"` yields the payloads bare. With a list of modes, items arrive as `(mode, payload)` so you can branch. The typical mapping in a UI: step updates drive a coarse stage indicator, custom payloads drive a fine-grained progress bar or a live log line, message chunks drive the answer body. ## Where teams get this wrong - Using custom writes as a logging mechanism. Logs belong in your logger and your tracing backend, which persist and are searchable; the custom stream vanishes when the connection closes. - Emitting per-item events in a tight loop, flooding the socket. Throttle — every N items or every N hundred milliseconds. - Depending on custom events for correctness, for example having the client compute the final answer by accumulating custom payloads. If the connection drops, that information is gone; anything load-bearing belongs in state. - Forgetting the async/3.10 case and shipping a node whose progress works locally on a newer interpreter and silently stops in an older runtime image.

  • Does a custom payload become part of graph state?
    No. It is emitted straight to stream consumers, never merged into a channel, never run through a reducer, and never written by a checkpointer. If a downstream node needs the value, the node must also return it as a normal state update. Treat custom writes as telemetry for the live connection, not as data.
  • What happens to custom events when a run is resumed from a checkpoint?
    They are not replayed, because nothing persisted them. A resumed run emits only the custom payloads produced from the resume point onward, so a client that rebuilds its view after reconnecting must derive it from state it can read rather than from a complete history of custom events.
  • When must you take the writer as a node parameter instead of looking it up?
    For async node bodies on Python 3.10, where the writer is not propagated through the async context and the implicit lookup will not find it. Declaring a StreamWriter-annotated parameter has the runtime inject it explicitly, which works on every supported version, so it is the safer default in libraries.
  • Your custom payloads never reach the consumer and nothing errors. What do you check first?
    Whether the code is really executing inside a graph invocation. Outside a run the writer resolves to a no-op, so direct calls to the node function in a test or a script swallow every write silently. After that, check that the consumer actually requested the custom mode — a stream configured for another mode simply never yields those payloads.

saying these in an interview costs you the question

  • Expects custom payloads to appear in the final state
  • Assumes checkpoint resume replays earlier custom events
  • Uses the custom stream as the application log
  • Emits an event per loop item with no throttling
  • Puts internal data in payloads that reach the browser

context