Your AI pipeline has three steps. Transcribe the audio. Summarize the transcript. Generate an email from the summary. The email comes out wrong. Rambling, off-topic, misses the key points from the meeting.

Which step broke?

Maybe the transcription was inaccurate and the summarizer worked from garbage. Maybe the transcription was perfect but the summary prompt was too aggressive, compressing a 30-minute meeting into one sentence. Maybe both were fine and the email generation prompt had a formatting bug. You don't know. The pipeline gave you one output and hid three intermediate results.

Without tracing, you'd re-run each step individually, inspect each output by hand, and manually correlate timestamps across log entries to figure out what belonged to the same run. With correlation IDs, you query one string and see every step's input and output in order.

Why Multi-Step Pipelines Are Hard to Debug

AI workflows chain models together. Step 1 calls a speech-to-text engine. Step 2 sends the transcript to an LLM for summarization. Step 3 sends the summary to another LLM to draft the email. Each step is a separate API call, potentially to a different backend running a different model. The output of step N becomes the input of step N+1.

When the final output is wrong, the failure could be at any step. And the further down the chain the failure is caught, the harder it is to trace backward. A bad transcription corrupts every downstream step. A bad summary only corrupts the email. But from the outside, both look the same: the email is wrong.

The traditional approach: add print statements at each step, re-run the pipeline, read terminal output, and correlate entries manually by timestamp. This works for three steps on a quiet afternoon. It doesn't work for eight steps across three services when you're also running a dev server, a model loader, and a file watcher that are all printing to the same terminal.

The output you need is buried in noise. Or it scrolled past. Or it's in a different terminal tab. The data existed. It was printed. And now it's gone.

What Is a Correlation ID?

A correlation ID is a string that tags every log entry from a single execution. All events that belong to the same pipeline run share the same ID. Query by that ID and you get the complete timeline of one execution, across all components and processes.

The concept isn't new. Distributed tracing systems like Jaeger and Zipkin have used trace IDs for years. The idea is the same. The difference is that you don't need a distributed tracing infrastructure to use correlation IDs in LogPiper. You pass a string when you log. You query by that string when you debug. No agents to install, no collectors to configure, no dashboard to set up.

The ID itself can be anything. A UUID. A timestamp-based string. A job name. The only requirement is uniqueness per execution. If two pipeline runs share the same ID, their logs merge and the trace is useless.

How LogPiper Handles Correlation IDs

LogPiper supports correlation IDs at two levels: automatic and manual.

Automatic correlation

When ToolPiper executes a workflow through its pipeline system, it generates a correlation ID (format: exec_abc12345) and attaches it to every log entry in that execution. HTTP requests to the inference engine, responses from the model, engine events, workflow step completions - all tagged with the same ID. No instrumentation needed. The tracing happens because the system was built for it.

If your pipeline runs transcription, then summarization, then text-to-speech through ToolPiper's API, and any of those calls fail or produce unexpected output, the correlation ID already groups them. You query one string and see the entire chain.

Manual correlation

Your own code can pass a correlationId field when logging. This is how you trace pipelines that you orchestrate yourself.

import requests
from uuid import uuid4

job_id = f"job_{uuid4().hex[:8]}"

def log(level, event, message, data=None):
    requests.post("http://127.0.0.1:9998/log", json={
        "source": "my-pipeline",
        "level": level,
        "event": event,
        "message": message,
        "data": data or {},
        "correlationId": job_id
    }, timeout=2)

log("info", "pipeline.start", "Starting audio processing")

# Step 1: Transcribe
transcript = transcribe(audio_file)
log("info", "transcribe.complete", "Transcription done",
    {"words": len(transcript.split()), "duration_s": 4.2})

# Step 2: Summarize
summary = summarize(transcript)
log("info", "summarize.complete", "Summary generated",
    {"sentences": summary.count("."), "input_words": len(transcript.split())})

# Step 3: Generate email
email = generate_email(summary)
log("info", "email.complete", "Email drafted",
    {"length": len(email), "subject": email.split("\n")[0]})

Every entry shares job_id. Every entry is queryable as a group. The ID was generated once, at the top of the pipeline, and threaded through every step.

Querying a Trace

Once events are tagged, pull the full trace with one request:

curl "http://127.0.0.1:9998/logs?correlationId=job_a1b2c3d4"

You get every event from that execution, sorted by timestamp:

[
  {"event": "pipeline.start", "message": "Starting audio processing", ...},
  {"event": "http.request", "data": {"url": "/v1/audio/transcriptions", "method": "POST"}, ...},
  {"event": "http.response", "data": {"status": 200, "responseBody": "...full transcript..."}, ...},
  {"event": "transcribe.complete", "data": {"words": 1247, "duration_s": 4.2}, ...},
  {"event": "http.request", "data": {"url": "/v1/chat/completions", "requestBody": "...prompt + transcript..."}, ...},
  {"event": "http.response", "data": {"status": 200, "responseBody": "...summary..."}, ...},
  {"event": "summarize.complete", "data": {"sentences": 3}, ...},
  {"event": "http.request", "data": {"url": "/v1/chat/completions", "requestBody": "...prompt + summary..."}, ...},
  {"event": "http.response", "data": {"status": 200, "responseBody": "...email draft..."}, ...},
  {"event": "email.complete", "data": {"length": 842}, ...}
]

Reading top to bottom, you see each step's input and output. If the transcript was wrong, you see it in the first http.response entry. If the summary was too short, the second response body shows you what the LLM actually produced. If the email prompt was malformed, the third request body reveals the exact prompt that was sent. One query, complete visibility.

Example: The Summary Missed Key Points

A user runs an audio-to-email pipeline. The resulting email misses half the action items from the meeting. Something went wrong, but what?

Query the correlation ID. The trace shows:

Transcription output (step 1): Accurate. All speakers captured. Action items clearly spoken: "We need to ship the API changes by Friday" and "Maria will handle the client follow-up." Both present in the transcript.

Summarization request (step 2): The system prompt reads: "Summarize the following in one sentence."

Found it. The user wanted a detailed summary with action items. The prompt asked for one sentence. The summarizer did exactly what it was told. It compressed a 30-minute meeting into a single sentence and dropped everything that didn't fit.

Root cause identified in one query. The transcription was fine. The email generation was fine. The summarization prompt was wrong. Fix the prompt, re-run, done.

Without the trace, the AI assistant would have started by questioning the transcription quality, then maybe the email prompt, then maybe the model choice. Three rounds of elimination before landing on the actual problem. The correlation ID turns that into a direct lookup.

Example: Pipeline Crash at Step 2

The pipeline stops mid-execution. Step 1 completes. Step 2 never finishes. No final output at all.

Query the correlation ID:

curl "http://127.0.0.1:9998/logs?correlationId=exec_f9e8d7c6"

The trace shows three entries:

  1. transcribe.complete - step 1 succeeded, 1,247 words transcribed
  2. http.request - summarization request sent to /v1/chat/completions
  3. http.error - status 503, response body: {"error": {"type": "model_not_loaded", "message": "llama-3.2-3b is not loaded. Call /engine/load first."}}

The model wasn't loaded. The transcription backend (a different engine) was running fine, but the LLM for summarization hadn't been loaded into memory. The fix is either to load the model before the pipeline starts or to have the pipeline call the load endpoint as a prerequisite step.

The response body told you exactly what happened and what to do about it. Without the trace, you'd see a generic connection error in the terminal and start guessing: network issue? Wrong port? Service crashed? The actual cause - model not loaded - was in the response body that LogPiper captured.

Tracing Across Processes

Correlation IDs work across process boundaries. If your Python script calls ToolPiper's API, and ToolPiper generates its own log entries during that request, both your entries and ToolPiper's entries can share the same correlation ID. When ToolPiper propagates the ID internally, the trace spans your app and the backend.

For your own multi-service architecture, pass the correlation ID through HTTP headers or request bodies. The pattern is straightforward:

# Service A: the orchestrator
job_id = "batch_20260404_001"
response = requests.post("http://localhost:3001/process",
    json={"data": payload, "correlationId": job_id})
log("info", "dispatch.sent", "Sent to processor", correlationId=job_id)

# Service B: the processor
correlation_id = request.json.get("correlationId")
log("info", "process.received", "Processing started", correlationId=correlation_id)
result = do_work(request.json["data"])
log("info", "process.complete", "Processing done",
    data={"result_size": len(result)}, correlationId=correlation_id)

Both services log to the same LogPiper instance at 127.0.0.1:9998. Query by batch_20260404_001 and you see the orchestrator's dispatch, the processor's receipt, and the processor's completion - all in one timeline. Two processes, one trace.

This is the same principle that distributed tracing systems use. The difference is that LogPiper keeps it simple. No trace context propagation libraries. No span hierarchies. No collector agents. You pass a string and you query a string. For local development with a handful of services, that's enough.

When to Use Correlation IDs

Multi-model AI pipelines. STT, then LLM, then TTS. Or: embed, then retrieve, then generate. Any chain where model outputs feed into other models. Tag the chain and trace the data flow when the final output is wrong.

Batch processing jobs. Tag each batch with a unique ID. When batch 47 of 200 fails, query its ID and see exactly which item caused the failure and what the error was.

Request tracing across services. A frontend calls a backend, which calls an inference engine, which calls a model. Pass the correlation ID through each hop and the full request lifecycle is one query away.

Comparing pipeline runs. Run the same pipeline twice with different parameters. Give each run a different correlation ID. Query each trace separately. Compare the outputs side by side to see where they diverge. Did changing the temperature from 0.3 to 0.7 affect the summary quality? The traces show you the exact outputs from each run.

Workflow debugging with AI assistants. When you tell Claude Code or Cursor to debug a pipeline failure, point the AI at the correlation ID. The AI queries one endpoint and gets the full context: every step, every request, every response. No manual log file grepping. No "can you paste the error message" back-and-forth.

What Correlation IDs Don't Do

Correlation IDs are optional. If you don't pass one, your log entries aren't grouped. They're still in LogPiper and still queryable by level, source, event type, or time range, but you lose the ability to pull a single execution as a unit.

ToolPiper auto-generates IDs for its own workflow executions. But your custom code needs to pass them explicitly. There's no automatic propagation across HTTP boundaries unless you build it into your request protocol. If service A calls service B and doesn't include the correlation ID in the request, service B's logs are orphaned from the trace.

The IDs don't create parent-child relationships between entries. There's no span tree, no call graph, no waterfall visualization. You get a flat list of entries sorted by timestamp, all sharing one ID. For most local debugging, a flat timeline is exactly what you need. If you need hierarchical traces with span nesting and dependency graphs, you need a proper distributed tracing system like Jaeger or Tempo.

And the 5,000-entry circular buffer in LogPiper means old entries get evicted. A correlation ID can't retrieve entries that have already been pushed out of the buffer. For long-running pipelines that generate a lot of log traffic, export the buffer periodically or query the correlation ID before the entries age out.

Making It Automatic

The best correlation ID strategy is one you don't have to think about. Two patterns work well.

Generate at the top, thread through everything. Create the ID once when the pipeline starts. Store it in a variable that every step can access. Every log call, every HTTP request, every error handler includes the ID. You write this code once per pipeline and it traces every run from that point forward.

Add it to your CLAUDE.md for AI-assisted debugging. If you're using an AI coding assistant, add instructions to your project config:

## Pipeline Debugging
When instrumenting pipeline code, always generate a correlation ID
at the start and pass it to every log call:
  correlationId = f"run_{uuid4().hex[:8]}"
When debugging pipeline failures, query by correlation ID:
  curl "http://127.0.0.1:9998/logs?correlationId=<id>"

The AI assistant follows the convention. Every pipeline it instruments gets a correlation ID. Every debugging session starts with a trace query. The discipline is encoded in the project, not in your memory.

Try It

ToolPiper is free from the Mac App Store. LogPiper runs automatically, and correlation IDs work the moment you start passing them.

This is spoke 8 in the vibe debugging series on AI development observability. For the complete LogPiper reference, see LogPiper: A Universal Logging Bus That Ships Free Inside ToolPiper. For how full HTTP body capture works alongside correlation IDs, see Full HTTP Body Capture: See Exactly What Your AI Sent and Received.