Context Propagation
A setup guide for ensuring traces and spans remain connected across async boundaries.
Context propagation ensures that all enforcement calls, traces, and spans are correctly correlated within a single request or user session. Without it, your traces will fragment, preventing you from diagnosing what caused a policy block.
Why Context Matters
If you skip context propagation, you lose:
- Trace continuity: Spans from the same request appear disconnected, making timeline debugging impossible.
- Audit correlation: You cannot link a blocked
enforcePostdecision back to the originating prompt or user ID. - Support triage: You cannot filter the Console logs by
user_idororg_idwhen investigating customer tickets.
Operational IDs
The following fields must be injected at the absolute edge of your application (usually the API middleware):
| Field | Purpose | Example |
|---|---|---|
requestId |
Essential for grouping spans within an immediate application execution lifecycle. | req_abc123 |
traceId |
Essential for distributed tracing across multiple microservices (e.g., from your API gateway to your agent runner). | trace_xyz789 |
orgId |
The tenant identifier. Mandatory for correct multi-tenant policy resolution. | org_acme |
userId |
The end-user identifier, used for incident triage and support tickets. | user_42 |
tags |
Arbitrary key-value metadata. | { env: "prod" } |
Standard Middleware Setup
The most robust way to guarantee context is to use the provided SDK middlewares at your application entrypoint. The middleware manages the requestId generation, reads distributed headers (x-njira-trace-id), binds the context to the thread, and flushes traces on exit.
Express / Node.js
import { njiraMiddleware } from "@njiraai/sdk/express";
// Place this strictly before your LLM/Agent route handlers
app.use(njiraMiddleware(njira));
FastAPI / Python
from njiraai.fastapi import NjiraMiddleware
# Binds contextvars automatically per-request
app.add_middleware(NjiraMiddleware, njira=njira)
Next.js
// middleware.ts
export { njiraMiddleware as middleware } from "@njiraai/sdk/next";
Manual Context (Worker/Job Queues)
If you are running agents inside background workers (e.g., Celery, BullMQ, Temporal) where HTTP middleware does not apply, you must initialize context manually.
TypeScript (AsyncLocalStorage)
// Wrap the entire job processor execution
njira.runWithContext(
{
requestId: job.id,
orgId: job.data.tenant_id,
userId: job.data.user_id,
},
async () => {
// All SDK and span calls inside this closure will inherit the IDs above
await processAgentWorkflow(job.data);
}
);
Python (contextvars)
ctx = {
"request_id": job.id,
"org_id": job.tenant_id,
"user_id": job.user_id,
}
async def worker_handler():
# Will automatically attach the scoped ctx
await execute_agent_chain()
await njira.run_with_context(ctx, worker_handler)
Triage: Missing or Disconnected Traces
If you investigate a Block event in the Njira Console but cannot see the parent llm_request span, run through this troubleshooting checklist:
- Verify Middleware Order: Ensure the Njira middleware is mounted before body parsers and framework-specific LLM adapters. Use
njira.getContext()manually within the controller to verify that fields populated correctly. - Check the Async Boundary: If your application spins off isolated fire-and-forget promises (
Promise.all()without waiting, orasyncio.create_task()), localized context may be dropped. Ensure SDK boundaries are within the awaited call stack. - Console Log Dump: Inject
console.log(njira.getContext())immediately prior to the tool boundary call. If the object printsundefinedor lacks arequestId, your context scope has failed. - Third-Party Callbacks: Some legacy libraries (e.g., older Postgres callback drivers) destroy
AsyncLocalStoragestate. Prefer modern async/await drivers.