Skip to content

Tracing Workflows

LlamaIndex Workflows can emit rich trace data for each handler execution. This page shows how to:

  • Use the withTraceEvents middleware to add tracing hooks to any workflow
  • Enrich traces with the OpenTelemetry plugin to export spans to your chosen backend

Here are some use cases for tracing:

  • Debug complex workflows and concurrency issues
  • Measure latency and surface errors across handlers
  • Monitor workflow performance and identify bottlenecks

The withTraceEvents middleware wraps a workflow and lets you attach one or more trace plugins that decorate every handler. This is the simplest way to enable tracing behavior across your workflow.

import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events";
// Define events
const startEvent = workflowEvent();
// Assume you have a plugin (see sections below for real examples)
const myPlugin = (handler) => handler; // no-op for illustration
// Enable trace events and attach your plugin(s)
const workflow = withTraceEvents(createWorkflow(), {
plugins: [myPlugin],
});
workflow.handle([startEvent], () => {
// business logic
});
const { sendEvent } = workflow.createContext();
sendEvent(startEvent.with());

To export spans and errors to a backend (or to the console during development), add the openTelemetry plugin from @llamaindex/workflow-otel and initialize the OpenTelemetry SDK.

Make sure to install the @llamaindex/workflow-otel package before using it:

Terminal window
npm install @llamaindex/workflow-otel

This example runs on Node.js and also uses the OpenTelemetry SDK, so make sure to install the used dependencies too:

Terminal window
npm install @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @opentelemetry/api
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events";
import { openTelemetry } from "@llamaindex/workflow-otel";
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
// Initialize OpenTelemetry SDK (use your preferred exporter in real deployments)
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()),
});
sdk.start();
// Define events
const startEvent = workflowEvent();
const stepEvent = workflowEvent<{ value: string }>();
// Create workflow and attach the OpenTelemetry plugin
const workflow = withTraceEvents(createWorkflow(), {
plugins: [openTelemetry],
});
// Handlers automatically produce spans (including errors)
workflow.handle([startEvent], (context) => {
context.sendEvent(stepEvent.with({ value: "hello!" }));
context.sendEvent(stepEvent.with({ value: "crash!" })); // demonstrates error spans
});
workflow.handle([stepEvent], (_context, event) => {
if (event.data.value === "crash!") {
throw new Error("The ultimate error happened!");
}
});
// Run
const { sendEvent } = workflow.createContext();
sendEvent(startEvent.with());

Running this example, using the console exporter, you will see JSON span output including:

  • Trace and span IDs for correlating handler executions
  • Attributes and events (e.g., host/process metadata)
  • Error status and exceptions for thrown errors
  • Duration metrics per handler

Swap the exporter to send spans to systems like Jaeger, Honeycomb, or any OpenTelemetry-compatible backend by configuring the NodeSDK accordingly (for example, using OTLP exporters).

See the runnable demo source for a complete setup: open-telemetry.ts.

You can build your own plugin using createHandlerDecorator. A plugin produced this way can be passed directly to withTraceEvents({ plugins: [...] }).

Below is a minimal timing plugin that measures handler duration and logs it.

import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import {
withTraceEvents,
createHandlerDecorator,
} from "@llamaindex/workflow-core/middleware/trace-events";
const startEvent = workflowEvent();
// Create a decorator-based plugin
type Timing = { startedAt: number | null };
const timingPlugin = createHandlerDecorator<Timing>({
debugLabel: "timing",
getInitialValue: () => ({ startedAt: null }),
onBeforeHandler:
(h, _ctx, metadata) =>
async (...args) => {
metadata.startedAt = Date.now();
try {
return await h(...(args as any));
} finally {
const durationMs = Date.now() - (metadata.startedAt ?? Date.now());
console.log("[trace] handler duration (ms):", durationMs);
}
},
onAfterHandler: () => ({ startedAt: null }),
});
// Attach your plugin to the workflow
const workflow = withTraceEvents(createWorkflow(), {
plugins: [timingPlugin],
});
workflow.handle([startEvent], async () => {
await new Promise((r) => setTimeout(r, 50));
});
const { sendEvent } = workflow.createContext();
sendEvent(startEvent.with());