npx skills add ...
npx skills add datadog-labs/agent-skills --skill llm-obs-trace-rca
npx skills add datadog-labs/agent-skills --skill llm-obs-trace-rca
Root cause analysis on production LLM traces. Diagnoses why an LLM application is failing — works from eval judge verdicts, runtime errors, or structural anomalies depending on what signals are present. Walks the span tree from symptom to root cause. Use when user says "what's wrong with my app", "why is my eval failing", "analyze errors", "root cause analysis", "diagnose failures", or wants to understand production failure patterns.
Detection — At the start of every invocation, before taking any action, determine which backend to use:
--backend pup anywhere in their invocation → use pup mode immediately, regardless of whether MCP tools are present. Skip steps 2–4.mcp__datadog-llmo-mcp__list_llmobs_evals appears in your available tools.pup is executable: run pup --version via Bash. A JSON response containing "version" confirms pup is available."Neither the Datadog MCP server nor the pup CLI is available. Connect the MCP server (
claude mcp add --scope user --transport http datadog-llmo-mcp 'https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=llmobs') or install pup."
--backend pup is accepted anywhere in the invocation arguments and is stripped before passing remaining args to the skill logic.
pup invocation rules:
pup llm-obs <subcommand> [flags][{"type": "text", "text": "<json>"}]).pup auth login and stop.1h, 7d, 30m) and RFC3339 timestamps. Do not use now--prefixed strings — strip the prefix when converting from a skill --timeframe argument: now-7d → 7d, now-24h → 24h, now-30d → 30d.--summary on pup llm-obs spans search strips payload fields to essential metadata only. Use it in bulk/search phases where content is not needed.Invocation ID: At the very start of each invocation, before any MCP tool call, generate an 8-character hex invocation ID (e.g., 3a9f1c2b). Keep it constant for the entire invocation.
Intent tagging: On every MCP tool call, prefix telemetry.intent with skill:llm-obs-trace-rca[<inv_id>] — followed by a description of why the tool is being called. On the first MCP tool call only, use skill:llm-obs-trace-rca:start[<inv_id>] — instead (note the :start suffix). Example first call: skill:llm-obs-trace-rca:start[3a9f1c2b] — Phase 0: discover configured evals for task-cruncher to infer analysis mode
Diagnose why an LLM application is failing by searching production traces and walking the span tree from symptom to root cause. The skill automatically selects the best analysis mode based on available signals:
| Mode | Signal | When auto-selected |
|---|---|---|
| Eval Signal | LLM judge verdicts and reasoning (pass/fail rates, scoring) | Evaluators are configured for the app |
| Error Signal | Runtime errors (@status:error, error types, stack traces) | No evals configured, or user explicitly asks about errors/crashes |
| Generic | Structural anomalies (latency, agent loops, retrieval misses) | Explicit mode=generic override, or no strong signal found in Phase 1 |
The mode is announced (never asked) in the first user-facing output with a one-line override hint.
Resolve → Search → Observe → Open Coding → Axial Coding → Root Cause Navigation → Recommendations
| Input | Required | Default | Description |
|---|---|---|---|
ml_app | Yes (or eval_name) | — | The application to analyze. |
eval_name | No | — | One or more evaluators to focus on. Implies Eval Signal mode. Pass a list for multi-eval analysis. |
timeframe | No | now-24h | How far back to look. |
mode | No | inferred | Explicit mode override: eval, errors, generic. Skips inference entirely. |
failure_filter | No | — | Narrowing scope: "errors" (routes to Error Signal path), "high latency" (post-fetch duration sort), "low scores on <eval>" (promotes to eval_name), a tool name or span name (@name:<x> query). |
If neither ml_app nor eval_name is provided, ask the user.
| Tool | Purpose |
|---|---|
list_llmobs_evals | Discover all configured evals for an ml_app. Used in Phase 0 mode inference. |
get_llmobs_eval_aggregate_stats | Pass/fail rate or score distribution for an eval over a time window. |
get_llmobs_evaluator | Full evaluator config: prompt template, assessment criteria, span filter, sampling, provider. Use instead of the deprecated get_llmobs_eval_config. |
| Tool | Purpose |
|---|---|
search_llmobs_spans | Find spans by tags, span kind, status, query syntax. Paginate with cursor. Entry point for Phase 1. |
get_llmobs_span_details | Metadata, evaluations (scores, labels, reasoning), status, error fields, duration, and content_info map showing available fields + sizes. |
get_llmobs_span_content | Actual content for a span field. Supports JSONPath via path param for targeted extraction. |
get_llmobs_trace | Full trace hierarchy as span tree with span counts by kind. |
find_llmobs_error_spans | All error spans in a trace with error type, message, stack, and propagation context. |
expand_llmobs_spans | Load children of collapsed trace nodes. |
get_llmobs_agent_loop | Chronological agent execution timeline (LLM calls, tool invocations, decisions). May return empty — see Phase 4b fallback. |
get_llmobs_span_content patterns| Field | Path | What you get |
|---|---|---|
messages | $.messages[0] | System prompt (first message, usually system role) |
messages | $.messages[-1] | Last assistant response |
messages | (no path) | Full conversation including tool calls |
input / output | — | Span I/O |
documents | — | Retrieved documents (RAG apps) |
metadata | — | Custom metadata (prompt versions, feature flags, user segments) |
search_llmobs_spansAlways include @ml_app:"<ml_app>" in the query string — the structured ml_app parameter is unreliable and can return spans from other apps. Do not rely on the structured parameter alone.
Useful query fragments — combine with space (AND):
| Goal | Query |
|---|---|
| Errors only | @status:error |
| Eval is present on the span | @evaluations.custom.<eval_name>:* (presence only — pass/fail is read from get_llmobs_span_details, not the query) |
| A specific tool by name | @name:<tool_name> |
Dedicated params (span_kind, root_spans_only, ml_app) work alongside query, but query takes precedence over tags.
get_llmobs_span_details: Group span_ids by trace_id, chunk each trace's span_ids into batches of at most 20. Issue ALL chunks for a page in a single message.get_llmobs_span_content: Each call is independent — always issue ALL in a single message.get_llmobs_trace / find_llmobs_error_spans / get_llmobs_agent_loop: Parallelize across different traces in a single message.get_llmobs_span_details for page 1 results immediately — don't wait to collect all pages.Output discipline: Phases 0–5 are internal analysis. The only user-facing outputs during these phases are the Phase 1 Signal Summary and the mandatory checkpoints at Phases 2 and 3. Do NOT narrate reasoning, summarize intermediate findings, or output Phase 4 deep-dive results as prose. All detailed findings go exclusively into the Phase 6 report.
First: check for classification context. Scan the conversation for a # Session Classification Summary header. If found → enter Step 0S below and skip all remaining Phase 0 steps and Phase 1 entirely.
The canonical handoff format is the Per-Unit Details table inside the # Session Classification Summary section. Extract one row per unit:
| Field | Source |
|---|---|
trace_id | Link URL in the ID column: parse the trace_id= or session_id= query parameter from the link href |
verdict | Verdict column |
failure_mode | Failure Mode column (none for passing rows) |
detail | Reason column — use as the Phase 2 reasoning input (same role as eval judge reasoning or error messages) |
app_type | From the # Session Classification Summary header line (e.g. Root span kind: agent) — default LLM if absent |
Failure bucket = all rows where verdict is no or partial.
Present this overview before proceeding:
Then skip Phase 1 and jump directly to Phase 2. Carry forward:
(trace_id, span_id, detail) tuples — same structure as eval reasoning or error messagesapp_type from each trace block to choose the span navigation strategyStandard resolution (no classification context):
ml_app nor eval_name provided → ask the user. If eval_name is provided but ml_app is not → also ask for ml_app (eval names are not globally unique; without it, span searches return results from all apps sharing the eval name).timeframe not provided → default to now-24h.failure_filter (before mode inference):
"errors" → force Error Signal mode"low scores on <eval>" → treat as eval_name=<eval>, then continue inference"high latency" → note for Phase 1 (sort by duration post-fetch); continue inference@name:<x> query fragment for Phase 1; continue inferencemode was explicitly provided):
eval_name given → Eval Signallist_llmobs_evals_by_ml_app(ml_app):
eval_name is multi-valued, note for Phase 1: run parallel per-eval searches and merge+dedup by (trace_id, span_id).Three mode-specific paths. All end with a Signal Summary that labels the mode and includes a one-line override hint.
Mode switch handling: At any checkpoint, if the user says "switch to [error|eval|generic] mode", re-enter Phase 1 with the new mode. Phase 0 inputs do not re-resolve.
Auto-pivot: If the selected mode finds no data (0 evals configured, 0 error spans in timeframe), announce the pivot to Generic and proceed — do not stop and ask.
For each eval, call both in a single parallel batch:
get_llmobs_eval_aggregate_stats(eval_name, from, to)get_llmobs_evaluator(eval_name)Interpret aggregate stats:
total_count == 0 → Note "no data." Skip this eval (or pivot to Generic if it's the only one).pass_rate == 1.0 → Note "100% pass." Skip unless it's the only eval.Interpret eval config:
prompt_template, assessment_criteria, parsing_type, output_schema.Calibration cross-check: When two evals share a name prefix but differ in type (e.g. foo-boolean and foo-score), compare their pass rates on overlapping spans. A discrepancy >20% is an Evaluator Calibration Discrepancy — flag it in the report.
For each eval:
search_llmobs_spans(query="@evaluations.custom.<eval_name>:*", from, limit=50). When multi-valued, issue one search per eval in parallel — merge result sets, dedup by (trace_id, span_id).get_llmobs_span_details per trace_id batch (follow Parallelization Rules).JSON-type eval fallback: If @evaluations.custom.<eval_name>:* returns 0 spans but get_llmobs_eval_aggregate_stats confirmed total_count > 0, the eval is JSON-type and scores are not indexed on this field. Fall back to: search by the span name or span kind that the eval targets (check get_llmobs_evaluator for the span filter), then inspect output payloads for JSON verdict fields via get_llmobs_span_content(field="output").
For a single eval, collapse to a single-line header instead of a table.
search_llmobs_spans(query="@ml_app:\"<ml_app>\" @status:error", from=timeframe, limit=50). Paginate until ≥30 error spans or no more pages.
MCP tool spans sometimes report @status:ok but carry "isError": true in their output payload — these are invisible to @status:error queries and can outnumber hard errors.
Call search_llmobs_spans(query="@ml_app:\"<ml_app>\" @status:ok", span_kind="tool", from=timeframe, limit=20). For a sample of 5–10 results, call get_llmobs_span_content(field="output") in parallel. If any payloads contain "isError": true, add MCP soft errors as a separate row in the error frequency table with the note: (status:ok but isError:true in payload — not queryable via @status:error).
Group spans by error_type tag → frequency table. If error_type tag is absent on some spans, supplement with the error.type field from get_llmobs_span_details (fetched in Step 1d).
For the top 3–4 error types by count, pick 2–3 representative trace IDs each. Call find_llmobs_error_spans(trace_id) in parallel across all selected traces. Extract:
get_llmobs_span_details on representative spans for each error type (follow Parallelization Rules). Extract content_info, span_kind, duration.
If list_llmobs_evals returned evals in Phase 0, call get_llmobs_eval_aggregate_stats for each enabled eval in parallel. Flag any enabled eval with total_count: 0 as Broken Eval Configuration — include in the Signal Summary anomaly table as a High severity row.
search_llmobs_spans(query="@ml_app:\"<ml_app>\"", root_spans_only=true, from=timeframe, limit=50). Apply failure_filter narrowing if present (tool/span name → @name:<x> query; "high latency" → sort result set by duration after Step 1c). Paginate until ≥30 spans.
get_llmobs_span_details per trace_id batch.
Partition spans using heuristics:
duration (latency outliers)non_cached_input_tokens ≈ input_tokens across LLM spans. If the app has stable system prompts (>1k tokens) and cache hit rate is 0%, flag as High severity — enabling cache_control: ephemeral on the system prompt would cut input token costs by 60–90%Inspect content_info and span_kind across collected spans. Drives Phase 4 strategy.
App profile (from content_info):
| Signal | App profile | Phase 4 strategy |
|---|---|---|
content_info has messages | LLM/chat app | Extract system prompt via messages[0], check conversation flow |
content_info has documents | RAG app | Check retrieval quality alongside LLM output |
Trace contains agent span kind | Agent app | Try get_llmobs_agent_loop first; if it returns empty use child-span reconstruction (see Phase 4b) |
messages.count > 10 | Long conversation | Check for context overflow |
content_info has metadata | Has custom metadata | Check for clustering by metadata values (prompt version, feature flags) |
LLM Experiments traces: If root spans have
span_kind: experimentand carryinput,output, andexpected_outputstructured fields, you are looking at a Datadog LLM Experiments trace. Each span represents one dataset record run. Read quality signal from the root span'sinput/output/expected_outputfields viaget_llmobs_span_content— not from LLM sub-span messages, which may contain stub or placeholder content. Evaluations attached to experiment spans are computed by the Experiments framework at run time and may not be registered as online Datadog evaluators (get_llmobs_evaluatorwill return 404 for them).
Where the root cause likely lives — by symptom span kind:
| Symptom span kind | Symptom looks like | But root cause is often in... |
|---|---|---|
llm | Bad LLM response (eval flagged, wrong output) | Parent agent (bad instructions), sibling retrieval (bad context), sibling tool (bad data) |
agent | Bad orchestration | Child spans (wrong tool calls, bad routing), full agent loop |
tool | Bad tool result | Parent LLM (passed wrong parameters), tool implementation |
workflow | Bad overall output | Child sub-spans (which step first deviated?) |
retrieval | Bad retrieval | Query construction (parent), index/embedding config (outside trace) |
Key insight: The signal — eval verdict, error message, latency outlier — flags one span in isolation. It's a symptom report, not a diagnosis. The root cause often lives in a different span: a parent that gave bad instructions, a sibling that provided bad context, or a child that made a wrong decision. Phase 4 navigates the tree to find it.
Goal: Read per-row evidence and propose initial, concrete failure categories. Pool all problematic rows together — categories should describe app behaviors, not which signal flagged them.
Per-row "reasoning input" by mode:
get_llmobs_span_detailsfind_llmobs_error_spansShortcuts:
Use per-row signal from Phase 1 — do NOT re-fetch. Only call get_llmobs_span_content(field="input"/"output") for spans where the reasoning is insufficient (generic, empty, or just a stack trace with no app context).
If eval config is loaded (Eval Signal), distinguish early:
Each pattern must be specific: "Agent called search instead of calculator for price computation" — NOT "tool issue."
Goal: 3–8 final categories, ranked by impact.
priority = count × severity (severity: high / medium / low).Goal: The signal flagged a span. That's the symptom. Navigate the trace tree to find the actual root cause — it's often in a different span.
For each of the top 3 categories, pick 2–3 representative traces:
For each representative trace, call in a single message:
get_llmobs_trace(trace_id) — span hierarchy; locate the symptom span and its parent/siblings/childrenfind_llmobs_error_spans(trace_id) — check for runtime errors anywhere in the traceRuntime vs behavioral: If errors exist on or near the symptom span, the root cause may be a runtime failure rather than a behavioral one. Check this first.
Distributed trace fallback: If get_llmobs_trace returns "cannot find parent" or an empty span list (common in Ray-based or multi-process execution), reconstruct the trace manually using get_llmobs_span_details on the span_ids collected in Phase 1, sorted by start_ms.
Use the symptom span kind (from Phase 1.5). Issue ALL calls in a single message.
If symptom is on an llm span (most common):
get_llmobs_span_content(field="messages", path="$.messages[0]") on symptom span — system promptget_llmobs_span_content(field="messages") on symptom span — full context receivedget_llmobs_span_content(field="documents") on sibling retrieval spans (if any)get_llmobs_span_content(field="input") on sibling tool spans (if any)get_llmobs_span_content(field="messages", path="$.messages[0]") on parent agent/workflow spanIf symptom is on an agent span:
get_llmobs_agent_loop(trace_id, span_id) — full decision timeline (try first; if it returns 0 iterations, use the fallback below)get_llmobs_span_details on child spans — sort by start_ms to reconstruct the execution timelineget_llmobs_span_content(field="input"/"output") on child spans that look wrongAgent loop fallback (when get_llmobs_agent_loop returns 0 iterations): Reconstruct the timeline from get_llmobs_span_details results sorted by start_ms. Group by span_kind to identify LLM → tool → LLM sequences. This fallback is frequently needed — get_llmobs_agent_loop returns empty for many apps.
If symptom is on a tool span:
get_llmobs_span_content(field="input") on symptom span — what parameters was it called with?get_llmobs_span_content(field="messages") on parent LLM span — did the LLM construct the call correctly?If symptom is on a workflow span:
get_llmobs_span_details on all child spans — find which step first deviatedget_llmobs_span_content(field="input"/"output") on the deviating childAlways also fetch:
get_llmobs_span_content(field="metadata") on the symptom span — clustering signals (prompt version, feature flags)For each category, trace the causal chain:
For suspected eval issues (Eval Signal, if config loaded): Compare eval criteria against evidence. Is the prompt ambiguous? Criteria too strict?
Root cause categories:
| Category | Description |
|---|---|
| System Prompt Deficiency | Instructions unclear, missing, or contradictory — in symptom span OR its parent |
| Tool Gap | Needed tool doesn't exist or parameters too coarse |
| Tool Misuse | Wrong tool called or wrong parameters — often visible in agent loop or parent LLM |
| Routing/Handoff Error | Wrong sub-agent selected (multi-agent systems) |
| Retrieval Failure | RAG returned irrelevant or missing context — check sibling retrieval spans |
| Context Overflow | Critical info lost due to context length |
| Upstream Data Issue | A sibling or parent span provided bad data that cascaded to the symptom span |
| Runtime Error | Tool/API failure, timeout, exception — from find_llmobs_error_spans |
| Evaluator Miscalibration | Eval criteria produce false positives/negatives (Eval Signal mode only) |
Goal: Concrete, actionable recommendations grounded in trace evidence. Actual text/code changes with before/after quotes from the trace — not generic advice.
Recommendation types: System Prompt Edit (quote actual prompt, provide before/after), Tool Gap/Misuse (reference agent loop steps), Routing/Handoff Fix, Retrieval Fix (show retrieved vs needed), Evaluator Prompt Edit (flag that eval changes need re-validation; Eval Signal only), Other.
When run in Claude Code with codebase access: Search the codebase for system prompt, tool definitions, or routing logic. Propose specific diffs. Always ask before modifying files.
Write the full report following the Output Format below. This is the primary deliverable — output it directly in the chat.
Do NOT take any action automatically. After presenting the report, ask the user what they'd like to do next:
llm-obs-rca-{ml_app}-{date}.mdpup notebooks create to create the notebook and pup notebooks edit NOTEBOOK_ID --file /tmp/nb_cells.json to append sections (see Tool Reference)now-7d if current window was now-24h)If the user chooses option 4, follow the notebook creation fallback pattern:
Call mcp__datadog-mcp-core__create_datadog_notebook with:
name: LLM Obs RCA: {ml_app} ({mode}) — YYYY-MM-DDtype: reporttime_span: 1wcells: one cell per section (see Notebook Cell Structure below)If the MCP call fails, inspect the error before giving up:
/tmp/nb_rca_{ml_app}.json as a full API envelope: {"data": {"attributes": {"name": "...", "time": {...}, "cells": [...]}, "type": "notebooks"}}pup notebooks create --file /tmp/nb_rca_{ml_app}.jsonAfter successful creation by either method, output the URL on its own line:
RCA report exported to notebook: <url>
Print the URL prominently — if /eval-bootstrap runs next in the same session, it will detect this URL and offer to append the evaluator suite to the same notebook.
| Cell | Content |
|---|---|
| 1 — Overview | Structured header (see Overview cell format below — follow it exactly) |
| 2 — Signal Summary | Mode-specific health table |
| 3 — Failure Taxonomy | Taxonomy table |
| 4…N — Failure Modes | One cell per failure mode |
| N+1 — Action Plan + Limitations | Action plan table + bullet list |
Notebook formatting rules (apply to every cell):
>) for prompts/rubrics, inline code (`) for short valuesThe Overview cell must follow this exact structure. No prose paragraphs. No inline-numbered findings. App description is one sentence maximum.
Mode-appropriate metric rows:
Eval Signal:
Error Signal:
Generic:
When entering from Step 0S (classification context), replace the Signal Summary table with:
Otherwise, mode-specific — pick the appropriate variant:
Eval Signal — one row per eval:
| Eval | Type | Total | Pass Rate | Status |
|---|---|---|---|---|
| eval_1 | boolean | 4,891 | 37.3% | ⚠ Investigating |
Error Signal — one row per error type:
| Error Type | Spans | Cascade? | Origin Span Kind |
|---|---|---|---|
| TimeoutError | 42 | Yes | tool |
Generic — one row per anomaly type:
| Anomaly Type | Count |
|---|---|
| Latency outliers (>p90) | 12 |
| # | Failure Mode | Traces | % | Severity | Root Cause |
|---|---|---|---|---|---|
| 1 | ... | ... | ...% | High | Tool Misuse |
| Priority | Action | Confidence | Impact |
|---|---|---|---|
| 1 | Fix monitor_groups_search schema — add group_states param | High | Eliminates ~21 spans/7d |
When mode is Generic and no evals are configured, always append as the final action plan row:
| N | Configure at least one evaluator | High | Enables Eval Signal mode for future RCAs — app currently has no ongoing quality signal |
Bullet list of what needs more data or follow-up action.
[Trace {first_8}...](https://app.datadoghq.com/llm/traces?query=trace_id:{full_32_char_id}).[{'type': 'text', 'text': '<json>'}]). Extract and json.loads() the inner payload if needed. Never assume MCP results are bare dicts or lists.This appendix applies only in pup mode. In MCP mode, use the tool names in the workflow sections directly.
| MCP Tool | pup Command |
|---|---|
search_llmobs_spans(query, ml_app, from, to, limit, cursor, root_spans_only, span_kind, summary) | pup llm-obs spans search --query "@ml_app:A [other_filters]" [--from F] [--to T] [--limit N] [--cursor C] [--root-spans-only] [--span-kind K] [--summary] — always use --query "@ml_app:A" to filter by ml_app; the --ml-app A flag is unreliable and silently returns spans from other apps. |
get_llmobs_span_details(trace_id, span_ids, from, to) | pup llm-obs spans get-details --trace-id T --span-ids S1,S2,... |
get_llmobs_span_content(trace_id, span_id, field, path) | pup llm-obs spans get-content --trace-id T --span-id S --field F [--path P] |
get_llmobs_trace(trace_id, include_tree) | pup llm-obs spans get-trace --trace-id T [--include-tree] |
get_llmobs_agent_loop(trace_id, span_id) | pup llm-obs spans get-agent-loop --trace-id T [--span-id S] |
find_llmobs_error_spans(trace_id) | pup llm-obs spans find-errors --trace-id T |
expand_llmobs_spans(trace_id, span_ids, max_depth, filter_kind) | pup llm-obs spans expand --trace-id T --span-ids S1,S2,... [--max-depth N] [--filter-kind K] |
| MCP Tool | pup Command |
|---|---|
list_llmobs_evals() | pup llm-obs evals list (filter by ml_app client-side) |
list_llmobs_evals_by_ml_app(ml_app) | pup llm-obs evals list-by-ml-app --ml-app A |
get_llmobs_evaluator(eval_name) | pup llm-obs evals get-evaluator EVAL_NAME |
get_llmobs_eval_aggregate_stats(eval_name, ml_app, from, to) | pup llm-obs evals get-aggregate-stats EVAL_NAME [--ml-app A] [--from F] [--to T] |
| MCP Tool | pup Command |
|---|---|
create_datadog_notebook(name, cells, ...) | pup notebooks create --title "TITLE" --file /tmp/nb_cells.json — confirm exact flags with pup notebooks create --help |
edit_datadog_notebook(id, cells, append_only=true) | pup notebooks edit NOTEBOOK_ID --file /tmp/nb_cells.json (fetches current notebook, appends provided cells, writes back) |
The cells file is a JSON array of cell objects:
## Signal Summary: `{ml_app}` · Error Signal
(No evals configured — analyzing runtime errors. Say `switch to eval mode` or `switch to generic mode` to change.)
**Timeframe**: {from} → {to} | **Total error spans sampled**: {N}
| Error Type | Spans | Cascade? | Origin Span Kind |
|------------|------:|:--------:|-----------------|
| TimeoutError | 42 | Yes | tool |
| APIError 429 | 18 | No | tool |
| ValueError | 7 | No | llm |
| MCP soft errors (isError:true) | 23 | No | tool |## Signal Summary: `{ml_app}` · Generic
(Analyzing structural anomalies. Say `switch to eval mode` or `switch to error mode` to change.)
**Timeframe**: {from} → {to} | **Sampled**: {N} root spans
| Anomaly Type | Count |
|---|:---:|
| Latency outliers (>p90) | 12 |
| Long agent loops (>8 iterations) | 5 |
| RAG retrieval misses | 3 |
| Zero prompt cache utilization | All LLM spans |
| Broken eval configurations | 2 |**Open coding**: {N} problematic rows → {K} initial categories: {Category1} ({count}), {Category2} ({count}), ...**Axial coding**: {merges/splits/drops}. Final categories:
1. {Category} ({count}, {pct}%) — {severity}
2. ...## `{ml_app}` · {Eval Signal | Error Signal | Generic} · {timeframe}
**Date**: {YYYY-MM-DD} | **Profile**: {short app profile} | **Model**: `{model(s)}`
{One sentence: what does this app do?}
| Metric | Value |
|--------|-------|
{mode-appropriate rows — see below}
### Findings
- **{Finding 1}** (~{pct}%): one-line root cause description
- **{Finding 2}** (~{pct}%): one-line root cause description
- **{Finding 3}** (if present): one-line root cause description
### Recommendations
- **{Recommendation 1}**: specific next step tied to Finding 1
- **{Recommendation 2}**: specific next step tied to Finding 2
*Sample: {N} spans analyzed. Confidence: High | Medium | Low — {one-line reason if Medium or Low}.*| Eval | `{eval_name}` ({type}) |
| Spans evaluated | {total_count} |
| Pass rate | {pass_rate}% ({pass_count} pass / {fail_count} fail) |
| Top failure mode | {name} (~{pct}%) |
| Evals configured | {N} || Error spans | {N} confirmed |
| Top error type | `{type}` ({pct}%) |
| Affected operation | `{span_name}` |
| Cascade pattern | Isolated / Cascading |
| Evals configured | {N} (none = no quality signal) || Spans sampled | {N} root spans |
| Top anomaly | {type}: {count} spans |
| Error spans | {N} (0 = structurally healthy) |
| Evals configured | {N} (none = no quality signal) |## Classification Signal Summary
**Source**: llm-obs-session-classify | **ml_app**: {app} | **Signal**: content-only | content+evals
| Metric | Value |
|--------|-------|
| Traces classified | N |
| Failures in corpus (no+partial) | F |
| Pass rate | X% |
| Failure modes | list |
*Root cause analysis is based on per-trace classification verdicts, not automated eval judge reasoning.*## Failure Mode N: [Name]
**Count**: {n} spans, {t} traces | **Severity**: High/Medium/Low | **Root Cause**: [Category]
[3–5 sentences: what goes wrong, when, what triggers it, causal chain.]
**Evidence**
{Use the mode-appropriate column set:}
Eval Signal — Trace | Judge verdict | What the trace revealed:
| Trace | Judge verdict | What the trace revealed |
|---|---|---|
| [69de86a7...](https://app.datadoghq.com/llm/traces?query=trace_id:{full_id}) | fail | Parent agent has no date format instruction |
Error Signal — Trace | Behavior | Version:
| Trace | Behavior | Version |
|---|---|---|
| [69de86a7...](https://app.datadoghq.com/llm/traces?query=trace_id:{full_id}) | 7 parallel calls, all 400 | v107624932 |
Generic — Trace | Anomaly | Signal:
| Trace | Anomaly | Signal |
|---|---|---|
| [69de86a7...](https://app.datadoghq.com/llm/traces?query=trace_id:{full_id}) | 94s, 12 tool calls | Latency outlier |
{For tool misuse — add a tool inputs table:}
**Tool inputs (100% of sampled calls)**
| Argument | Value passed (wrong) | Correct approach |
|---|---|---|
| `query` | `"monitor_id:123 group_status:alert"` | `"monitor_id:123"` (name/tag only) |
{For Eval Signal — add judge reasoning as a blockquote:}
> "{quoted judge reasoning}"
**Root cause**: [WHY this happens — specific span, parameter, or prompt.]
**Fix**:
BEFORE: [actual text from trace]
AFTER: [proposed replacement]
**Impact**: Eliminates ~{n} spans / {timeframe}.