How to Fix 'callback not firing during development' in LangGraph (Python)
What this error usually means
If your LangGraph callback is not firing during development, the graph is usually running, but the callback handler is not attached to the execution path you think it is. In practice, this shows up when using stream(), invoke(), or async graph execution and expecting LangChain/LangGraph callbacks to print tokens, node events, or trace data.
The common pattern: everything works in production or in one code path, then during local dev you get no callback output at all. The graph completes, but your BaseCallbackHandler methods like on_chain_start, on_llm_new_token, or on_chain_end never fire.
The Most Common Cause
The #1 cause is passing callbacks in the wrong place.
In LangGraph, callbacks need to be attached to the runnable invocation context. If you pass them to a constructor, store them on the wrong object, or expect them to magically propagate through a custom node function, they will not fire.
Broken pattern vs fixed pattern
| Broken | Fixed |
|---|---|
| Callback passed to graph compile/build logic or ignored inside node | Callback passed in config={"callbacks": [...]} at invocation time |
| Uses a plain Python function that bypasses LangChain run context | Uses a Runnable/LLM call with config propagation |
# BROKEN
from langchain.callbacks.base import BaseCallbackHandler
from langgraph.graph import StateGraph, END
class DebugHandler(BaseCallbackHandler):
def on_llm_new_token(self, token: str, **kwargs):
print("TOKEN:", token)
def call_model(state):
# This may run, but callbacks won't fire if config isn't propagated
response = llm.invoke(state["messages"])
return {"messages": response}
graph = StateGraph(dict)
graph.add_node("model", call_model)
graph.set_entry_point("model")
graph.add_edge("model", END)
app = graph.compile()
# Wrong: callbacks are not attached here
result = app.invoke(
{"messages": [{"role": "user", "content": "hello"}]}
)
# FIXED
from langchain.callbacks.base import BaseCallbackHandler
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END
class DebugHandler(BaseCallbackHandler):
def on_llm_new_token(self, token: str, **kwargs):
print("TOKEN:", token)
def call_model(state, config: RunnableConfig):
response = llm.invoke(
state["messages"],
config=config # propagate callback context
)
return {"messages": response}
graph = StateGraph(dict)
graph.add_node("model", call_model)
graph.set_entry_point("model")
graph.add_edge("model", END)
app = graph.compile()
result = app.invoke(
{"messages": [{"role": "user", "content": "hello"}]},
config={"callbacks": [DebugHandler()]}
)
If you are using stream() instead of invoke(), the same rule applies. Callbacks must be attached through the runtime config and propagated into any nested runnable calls.
A lot of developers miss this because LangGraph nodes are just Python callables. If your node does internal work without forwarding config, LangChain has no way to connect that work to your callback handler.
Other Possible Causes
1. You are using a sync callback with async execution
If your graph runs with ainvoke() or astream(), but your handler only implements sync methods incorrectly, you can get silent no-op behavior.
# Problematic for async paths if you're expecting async hooks
class DebugHandler(BaseCallbackHandler):
def on_llm_new_token(self, token: str, **kwargs):
print(token)
# Better for async-heavy workflows:
class AsyncDebugHandler(BaseCallbackHandler):
async def on_llm_new_token(self, token: str, **kwargs):
print(token)
2. Your node bypasses LangChain runnables entirely
If your node calls raw SDK code directly, callbacks from LangChain will not see it.
def bad_node(state):
# Direct OpenAI/Anthropic SDK call here won't trigger LangChain callbacks
text = client.responses.create(...)
return {"answer": text}
Use a LangChain model wrapper or explicitly instrument the SDK yourself.
3. You forgot to pass config through nested nodes/tools
This is common in multi-step graphs.
def parent_node(state, config):
child_result = child_runnable.invoke(state["query"]) # missing config
return {"result": child_result}
Fix:
def parent_node(state, config):
child_result = child_runnable.invoke(state["query"], config=config)
return {"result": child_result}
4. Your environment hides output during development
Sometimes the callback fires, but your dev setup swallows stdout/stderr.
| Environment | Typical issue |
|---|---|
| Jupyter | Output buffering or cell execution order |
| Docker | Logs not attached to container stdout |
| VS Code debugger | Console routing hides prints |
If your handler uses print(), switch to structured logging:
import logging
logger = logging.getLogger(__name__)
class DebugHandler(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
logger.info("chain started: %s", serialized.get("name"))
How to Debug It
- •
Confirm the callback class is actually instantiated
- •Add a log in
__init__or right before passing it intoconfig. - •If you never see it constructed, you are debugging the wrong object.
- •Add a log in
- •
Check whether the node receives
config- •Add this inside your node:
def my_node(state, config=None): print("CONFIG:", config) - •If it is
None, callbacks cannot propagate downstream.
- •Add this inside your node:
- •
Verify the model/runnable call uses that same config
- •Compare these two:
llm.invoke(messages) llm.invoke(messages, config=config) - •The second one is what keeps LangChain tracing and callbacks alive.
- •Compare these two:
- •
Turn on verbose tracing and watch for real event names
- •Look for events like:
- •
on_chain_start - •
on_chain_end - •
on_llm_start - •
on_llm_new_token
- •
- •If none appear, your callback chain is broken before execution reaches LangChain runtime.
- •Look for events like:
Prevention
- •Always accept and forward
configin every LangGraph node that calls another runnable. - •Prefer LangChain wrappers over raw SDK calls when you need callback visibility.
- •Use one debugging handler early in development:
class DebugHandler(BaseCallbackHandler): def on_chain_start(self, serialized, inputs, **kwargs): ... def on_llm_start(self, serialized, prompts, **kwargs): ... def on_llm_new_token(self, token: str, **kwargs): ... def on_chain_end(self, outputs, **kwargs): ...
If you standardize on passing config through every layer of your graph, this error mostly disappears. The moment you drop that propagation chain — especially inside custom nodes — callbacks stop firing and debugging gets painful fast.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit