Skip to main content
Bitfab integrates with LangGraph and LangChain via a callback handler that automatically captures graph node execution, LLM calls, tool invocations, and retriever queries as traced spans. For LangGraph tools that need replay mocking, the first-class integration intercepts ToolNode execution so an output mock can be returned without running the tool against the real world. Canonical signatures: TypeScript reference · Python reference

Supported Languages

Working with plain LangChain (no LangGraph)? The same handler serves both, and the SDK exposes it under a LangChain name too: getLangChainCallbackHandler() / get_langchain_callback_handler() and the BitfabLangChainCallbackHandler class are aliases of their LangGraph counterparts. Use whichever name matches your stack; the behavior is identical.

Quick Start

Mock LangGraph tool calls on replay

Experimental (alpha): The tool replay integration uses public LangGraph APIs, but its API and matching behavior may change before it is stable. The callback-only tracing APIs above remain fully supported.
Callbacks observe tool execution but cannot prevent it. Use the first-class LangGraph integration when tools need output mocks during replay. It combines:
  • wrapTools() in TypeScript, or LangGraph’s native ToolNode hooks in Python, at the tool execution boundary
  • createInvoker() / create_invoker() for the normal graph entry point; it adds the callback handler and replayable root together
  • callbackHandler / callback_handler and wrapInvoke() / wrap_invoke() as low-level primitives for workflows with meaningful application work around the graph invocation
Python uses LangGraph’s native interception API directly: Bitfab supplies the two hook callables and the application remains responsible for constructing and configuring ToolNode. LangGraph JS does not currently expose equivalent tool-call hooks, so the TypeScript integration wraps each tool’s public Runnable invoke() method and returns a normal tools array for bindTools() and ToolNode. It does not subclass ToolNode or depend on protected internals. The invoker uses LangGraph’s public withConfig() / with_config() API, so invocation-time configuration and existing callbacks are preserved. Only the graph input is recorded as the replay root input; callback managers and runtime configuration are not serialized. For an async-only Python graph, use create_async_invoker() and replay the returned async callable. When input preparation or post-processing should be inside the same trace, keep using the low-level form: pass callbackHandler / callback_handler in the graph invocation config and wrap the full application function with wrapInvoke() / wrap_invoke(). By default, every integration-managed tool is marked for replay mocking. Pass mockToolsOnReplay: ["searchDocs"] or mock_tools_on_replay=["search_docs"] to mark only selected tools; pass false to leave them unmarked. Replay’s mock: "all" strategy and matching mock overrides can still substitute any integration-managed tool. The integration records a serializable view of LangGraph’s result and reconstructs a real ToolMessage or Command during replay. Every reconstructed tool message uses the current invocation’s tool-call ID, so a recorded result remains valid when the model generates a different ID. Recorded tool calls are matched by tool name and occurrence order within one replay item. If a tool is marked for replay mocking and that occurrence has no recorded output or matching override, Bitfab refuses to execute the live tool. LangGraph then applies the ToolNode’s configured error behavior. Use replay with mock: "none" only when running real tools is intentional.

Current alpha limitations

  • Repeated tools are order-sensitive. Calls are matched by tool name and occurrence order. If the same tool runs concurrently, or current code changes its call order, replay can select the wrong recorded occurrence. Use this integration first on graphs whose same-name tool calls have deterministic ordering.
  • Every tool call must cross the integration boundary. In TypeScript, use the array returned by wrapTools() everywhere the graph references those tools. In Python, construct each replayable ToolNode with the integration’s hooks. Calling the original tool directly is not intercepted and cannot be mocked.
  • TypeScript proxies the public invoke() method. Standard LangChain tools work through this boundary. Custom tool objects that depend on exact object identity, custom proxy behavior, or a path that bypasses invoke() may be incompatible.
  • Tool inputs and outputs must serialize. Native ToolMessage and Command results are supported. Streaming tool results and unusual custom return objects are not yet guaranteed to round-trip.
  • Fail-closed errors follow ToolNode error handling. When tool-error handling is enabled, LangGraph may turn a missing-recording error into an error ToolMessage and let the graph continue. Configure the node to propagate tool errors if a missing recording should abort the replay.
  • The convenience invoker covers invoke() / ainvoke(). For streaming, batch execution, or custom graph entry points, use callbackHandler / callback_handler and wrapInvoke() / wrap_invoke() directly.
Test the replay path with non-production dependencies before relying on it for a workflow with unsafe side-effects. Callback-only tracing does not provide this protection because callbacks run after tool execution has already started.

Plain LangChain Chains

The handler works the same way on LangChain chains and runnables; pass it in callbacks when invoking:

What Gets Captured

The callback handler hooks into LangChain’s callback system and creates spans automatically:

Trace Lifecycle

The first callback in a framework invocation becomes the trace root. For full LangGraph runs this is usually a chain/graph node; for plain LangChain usage it can also be a direct chat model, LLM, tool, or retriever call. The handler registers that root immediately as a pending external trace, then completes it when the root callback ends, so long-running invocations can appear as in progress before their final output is available. If the handler runs inside an active Bitfab withSpan / @span root, its spans attach to that outer trace instead. In that case the handler still records the framework span tree, but the outer Bitfab root owns final trace completion. You do not need an outer withSpan / @span root when the workflow is just the LangGraph or LangChain invocation. Add one only when there is meaningful application work around invoke() that should be part of the same trace, such as input preparation, retrieval outside LangChain, post-processing, persistence, or downstream service calls. When you do add an outer root, use the same trace function key for the outer span and the callback handler.

LangGraph Metadata

LangGraph-specific metadata is automatically extracted and stored as span context:
  • langgraph_step: Current step number
  • langgraph_node: Current node name
  • langgraph_triggers: What triggered this node
  • langgraph_path: Execution path
  • langgraph_checkpoint_ns: Checkpoint namespace

Token Usage

For LLM spans, token usage is captured from the LLM result. The handler prefers LangChain’s standardized usage_metadata on each generation’s message (which is also how usage arrives on the final aggregated chunk of streaming runs), then falls back to provider-native response_metadata shapes (OpenAI, Anthropic, Google Gemini / Vertex), and finally the legacy llm_output.token_usage location:
  • inputTokens: Prompt tokens. For Anthropic, cache reads and cache creation are added back so the value reflects the true prompt size.
  • outputTokens: Completion tokens
  • totalTokens: Total tokens
  • cachedInputTokens: Cached prompt tokens, from usage_metadata.input_token_details.cache_read, OpenAI prompt_tokens_details.cached_tokens, or Anthropic cache_read_input_tokens
  • model: Model name (extracted from serialized config or metadata)
When a result has multiple generations, usage is summed across them. Only provider-reported numbers are recorded: if the provider reports nothing (for example OpenAI streaming without stream_usage: true / stream_options: {"include_usage": true}), the fields are left unset rather than estimated.

TypeScript

Installation

Method Signature

Parameters:
  • traceFunctionKey (string, required): Groups all traces from this handler under one key in Bitfab
Returns: A BitfabLangGraphCallbackHandler that implements the LangChain callback handler interface (duck-typed, no @langchain/core dependency required).

Usage

Callback Hooks

LangChain invokes these callback hooks on the handler:

Nesting with Core Tracing

The handler integrates with Bitfab’s span stack. If the application has meaningful work around the graph or chain invocation, create a same-key withSpan wrapper around that outer workflow and the LangGraph spans nest as children. Handler-only instrumentation is enough for a plain LangGraph / LangChain call:
Use the same trace function key in both places. Both bitfab.getFunction(...) and bitfab.getLangGraphCallbackHandler(...) take a key; pass the same key ("my-pipeline" above) to both. If you use two different keys here, the same flow will register as two separate overlapping trace functions in the dashboard, an anti-pattern to avoid.

Error Handling

  • GraphBubbleUp: LangGraph’s internal interrupt mechanism. Detected automatically and completed silently (no error recorded).
  • All other errors: Error message captured in the span. The handler never throws; all callbacks are wrapped in try/catch.
  • Missing serialized callback data: Some framework versions can pass nullish serialized payloads. The handler still records the span using fallback names such as chain, llm, tool, or retriever.
  • Reusability: The handler resets after each root span completes and can be reused across multiple invocations.

Python

Installation

Method Signature

Parameters:
  • trace_function_key (str, required): Groups all traces from this handler under one key in Bitfab
Returns: A BitfabLangGraphCallbackHandler instance that extends BaseCallbackHandler from langchain-core.

Usage

Callback Methods

The handler implements these LangChain callback methods:

Nesting with Core Tracing

Bind the key once with get_function so the root and the handler share it (no repeated string to keep in sync). Use this only when the application has meaningful work around the graph or chain invocation; handler-only instrumentation is enough for a plain LangGraph / LangChain call:
Use the same trace function key for the root and the handler. Binding via get_function does this for you. The plain @bitfab.span("my-pipeline") / bitfab.get_langgraph_callback_handler("my-pipeline") forms work too, but you must pass the same key to both, otherwise the same flow registers as two separate overlapping trace functions in the dashboard, an anti-pattern to avoid.

Error Handling

  • GraphBubbleUp: LangGraph’s internal interrupt mechanism. Detected automatically and completed silently (no error recorded).
  • All other errors: repr(error) captured in the span. The handler never raises; all callbacks are wrapped in try/except.
  • Missing serialized callback data: Some framework versions can pass nullish serialized payloads. The handler still records the span using fallback names such as chain, llm, tool, or retriever.
  • Reusability: The handler resets after each root span completes and can be reused across multiple invocations.

Replay

Callback-only graphs are replayable, even though no @span / withSpan root exists in the application code. The handler records the graph invocation as the root span, with the initial graph state as the recorded input. Their observed framework calls run live during replay. To replay, pass the handler’s key plus a plain callable that re-invokes the graph (the SDK wraps it internally):
The key is the only link between the handler-recorded production traces and the replay callable. Rebuild runtime wiring inside the callable (config["configurable"] values, dependency objects, API keys); the trace records only the graph state. Put unsafe calls behind replay-mockable marked spans; use a no-op only for a replay-only callback slot with no recorded call to mock. For LangGraph tools that must not run, use getLangGraphIntegration() / get_langgraph_integration() as shown above. On SDKs that predate explicit-key replay, wrap the callable under the same key yourself (Python: @bitfab.span("weather-agent") with a (**state) signature; TypeScript: getFunction("weather-agent").withSpan(...)). Full details: Replaying handler-instrumented functions in the Python SDK and TypeScript SDK pages.