Skip to main content
The LangChain handler is in preview. Pin exactly (calado-langchain==0.0.1) until 1.0 — see Upgrading.

Overview

calado-langchain is a BaseCallbackHandler subclass. You attach it to a chain and LangChain itself dispatches the events calado needs — chain inputs and outputs, LLM messages, tool calls, retrievals, errors, latency, and the parent-child run tree. There is nothing to call from your own code. Compared to wrapping the LLM client directly, the callback handler captures the layer above: Use this page if your agent runs on LangChain or LangGraph and you do not already run a tracer. The callback handler is the path for LangChain users with no tracer in place. If you already trace your agent with LangSmith, use the LangSmith adapter instead. It forwards the runs LangSmith already collects and needs no package in your runtime. For raw anthropic / openai Python clients, send events with the Direct API.

LangGraph

LangGraph reuses LangChain’s callback system. The same handler works — node transitions arrive as nested chain runs and are rendered as a tree in the dashboard. No extra configuration.
LangGraph

Installation

Requirements: Python 3.9-3.13, langchain-core>=0.2.10,<0.5. Your API key is generated in your agent’s Settings page on app.calado.ai. See Quickstart for the step-by-step.

Quick start

Construct the handler with no arguments (it reads CALADO_API_KEY from the environment) and pass it on the chain’s config.
The session_id is optional — calado still captures runs without it — but strongly recommended. Without it, every top-level chain invocation is treated as a one-off and patterns are harder to surface.

Verifying it works

On the first successful POST, calado prints a one-line message so you know the wire is up:
Output is routed through Python’s logging module (logging.getLogger("calado")) and mirrored to stderr when no handler is attached, so the line is visible by default and easy to silence or redirect. For programmatic checks, call status():
Pass debug=True at construction for per-batch stderr lines:
If you saw the Connected line and events_sent > 0, the integration is working. Open your agent page on app.calado.ai to see the conversation.

Zero-touch mode

Set environment variables and skip the per-chain wiring entirely:
On import, the handler registers itself as a global LangChain callback and attaches to every chain in the process.
Global mode attaches the handler to every chain in the process. If you run mixed regulated and non-regulated agents in the same process, prefer per-chain mode and attach the handler only to the chains you want reported.

Scoping to one or more chains

Pass the handler on the chains you want reported. Group turns into a session by passing configurable.session_id:
Use any stable string: a session token, a database row id, a UUID. Without it, each top-level chain invocation is recorded as a one-off conversation. You can run multiple handler instances side-by-side with different API keys — for example, an orchestrator and a sub-agent reported as separate calado agents.

What gets captured

Sub-chains, LLM calls, tool runs, retrievers, embeddings, parsers, and prompts all become steps. The dashboard renders them as a tree under the conversation.

Redacting sensitive data

Pass a mask callable at construction. It runs in-process, per child run, before transport. Return a modified dict, or return None to drop just that step (the rest of the tree is captured).
Both sync and async functions are accepted. The handler probes once at construction (via a sentinel call) and caches whether to await the mask. If the mask raises 100 times in a row, the transport disables itself fail-closed and prints:
Mask patterns (emails, phone numbers, Presidio integration, stable placeholders) live on Redacting sensitive data. The package does not ship recipe helpers.

Runtime behavior

  • Sandboxed. LangChain catches and logs handler exceptions. calado cannot break your chain.
  • Streaming. Per-token events buffer in the accumulator and materialize on on_llm_end. One row per LLM call, not per token.
  • Batching. Events accumulate until batch_size (default 10) or flush_interval_s (default 30s) is reached. The first event of a process force-flushes immediately so you don’t wait 30 seconds for the Connected line.
  • Root-end flush. Each conversation is materialized on the root on_chain_end and queued atomically.
  • Crash safety. atexit drains the queue synchronously on the foreground thread with a 5s bounded timeout. Hard crashes and Ctrl-C may lose in-flight trees.
  • Retry. 5xx and network errors retry with backoff. 4xx responses drop immediately.
  • 401 auto-disable. After three consecutive 401 responses, the transport disables and prints one line:
  • Child-run cap. A single root run may have at most 950 child runs. Above the cap the server returns a structured 400 and the handler prints:
    In practice the 5 MB request body cap is often the binding constraint and is reached before 950 runs when payloads are large. Whichever limit you hit first, the fix is the same: split via configurable.session_id or reduce trace verbosity.
  • Hung runs. Accumulator entries older than max_run_age_s (default 1h) are evicted, counted in events_dropped, and logged at WARNING.
  • Logging. All output routes through logging.getLogger("calado"). Attach your own handler to route to Datadog, Sentry, structured logs, or silence it entirely. Stderr is the default mirror.
  • Threading. Transport runs on a dedicated daemon thread; it does not block your asyncio event loop.

API reference

Constructor

CaladoCallbackHandler is keyword-only. Positional construction is forbidden.
str
default:"env CALADO_API_KEY"
Agent API key. Generated in Settings on the agent page.
str
default:"https://app.calado.ai"
Override for self-hosted or staging environments.
int
default:"10"
Events accumulated before an HTTP flush.
float
default:"30.0"
Seconds before the background thread flushes a partial batch.
int
default:"3600"
Evict in-flight accumulator entries older than this many seconds.
int
default:"10485760"
Hard ceiling on queued bytes. On overflow the oldest event is dropped.
bool
default:"False"
Print one stderr line per batch flush.
Mask
default:"None"
Sync or async callable. See Redacting sensitive data.

Methods

Type aliases

For mypy --strict users (the package ships py.typed):

Ingest response shape

The POST /api/ingest endpoint returns:
The handler reads agent_id to render the deep link in the Connected log line and warns when the installed package version is below min_sdk_version. Older servers may omit these fields; the handler falls back to the generic View at https://app.calado.ai/agents URL.

Testing

Don’t construct the handler in tests. Without CALADO_API_KEY, construction logs a warning and transport stays disabled. For explicit teardown after construction:

Troubleshooting

[calado] Connected. never prints

Read in order:
  • enabled: False → see the 401 section below.
  • mask_failures > 0 → your mask is throwing. Fix and restart.
  • events_sent == 0 and last_error set → inspect last_error. Often a 4xx from a malformed payload or an unreachable base_url.
  • events_sent == 0 and no error → the chain hasn’t completed a root run yet, or CALADO_API_KEY was empty at construction.

401 from the server

calado auto-disables after three consecutive 401 responses. Check that CALADO_API_KEY starts with cl_ and matches an agent you own, then restart the process.

Inspect from a FastAPI endpoint

Mount the async status dict on a hidden route so on-call can read it without a deploy:
Use astatus() from async code — it wraps status() via asyncio.to_thread and does not block the event loop.

Run the doctor

Prints the installed calado-langchain version, the installed langchain-core version, the supported range, the Mask import status, and the fix command if pip resolution fails.

Upgrading

calado-langchain follows a pre-1.0 semver policy: any 0.x.y minor bump may introduce breaking changes. Pin exactly until 1.0:
After 1.0, normal semver resumes. Until then, read the CHANGELOG before each bump. The server returns a min_sdk_version on every ingest response (see Ingest response shape). When your installed version drops below it, the handler logs a WARNING via the calado logger.

Migrating after a langchain-core major bump

langchain-core is pinned to >=0.2.10,<0.5. When the next major lands, follow this template:
  1. Pin langchain-core and calado-langchain exactly in a feature branch.
  2. Run python -m calado_langchain doctor and confirm the supported range covers your target.
  3. Update both pins, then run the doctor again.
  4. Run your chain locally. Confirm the Connected line and one ingested conversation in the dashboard.
  5. Roll out behind the same flag as the langchain-core bump.

Production deployment

Uvicorn and Gunicorn

preload_app=False is the supported mode. The handler lazy-registers its atexit hook on the first event in each worker, so each worker drains its own queue cleanly on shutdown.
preload_app=True is a silent-loss landmine: the master process’s atexit races with each child’s atexit on fork. Use preload_app=False. If you must preload, set CALADO_DISABLE_ATEXIT=true and call handler.flush() from your shutdown hook explicitly.

AWS Lambda

Lambda sends SIGKILL on freeze, which skips atexit. Disable the hook and flush from your handler:

Google Cloud Run

Cloud Run sends SIGTERM (10s grace) then SIGKILL. Trap SIGTERM and call flush() before exit, or set CALADO_DISABLE_ATEXIT=true and rely on an explicit per-request flush.

Forked workers

The handler registers an os.register_at_fork(after_in_child=...) hook that clears the parent accumulator in the child. You don’t need to do anything — child workers start with a clean state.

Caps

Next: redacting sensitive data

Strip PII before events leave your process.