An enterprise customer forwards a screenshot to their account manager. Your AI agent told them their subscription included priority phone support. It doesn't, and never has. The account manager asks the support lead a reasonable question: why did it say that?
Most teams cannot answer. They have the email that went out and the email that came in. Everything between those two artifacts β what the agent understood, what it looked up, what it got back, what it decided and on what basis β happened inside a process that emitted a log line saying resolved: true and nothing else.
That gap is not a monitoring problem. Monitoring was working fine. It's an observability problem, and it's the difference between a five-minute answer and a shrug.
Monitoring tells you what. Observability tells you why.
The distinction gets muddled in vendor marketing, so it's worth being precise about it in the context of an email queue.
Monitoring is predefined questions with predefined answers. Containment rate by intent, escalation volume, p95 latency, error count. You decided in advance which numbers matter, and a dashboard shows them. It's genuinely useful and it's how you notice that something changed on Tuesday.
Observability is the ability to ask a question you didn't anticipate. Why did this ticket resolve the way it did? Which retrieved chunk produced that claim about phone support? Did the OMS call time out, or return a 200 with empty data that the agent then interpreted as "no orders found"? None of those questions were on anybody's dashboard, and all of them need per-event detail that aggregate metrics have already thrown away.
The analytics layer for an AI email queue answers the first kind of question well. This piece is about the second.
One more distinction worth drawing early, because these two things get conflated constantly: observability is not the same as an audit trail. Audit trails exist to satisfy a regulator, retain for six or seven years, and prove tamper-resistance. Traces exist to help an engineer at 3 p.m. on a Wednesday, live for two to thirty days, and are optimised for query speed over immutability. They overlap in what they record and differ completely in what they're for. Build both. Don't try to make one do the other's job.
The trace model, applied to an email
A trace is the complete record of one request through a system. In web infrastructure that request is an HTTP call. For an email agent, the natural unit is one inbound email through to a terminal outcome: sent, escalated, or dropped.
Inside the trace, each discrete operation is a span. Spans nest, each one carries a start time and a duration, and together they form a tree that shows both what happened and how long each part took.
A workable span taxonomy for an email agent looks like this:
- ingest β parsing, thread reconstruction, attachment extraction, sender identification. Cheap and boring until the day a customer's Outlook mangles the quoting and the agent replies to a six-month-old message in the chain.
- classify β intent detection, entity extraction, language detection, priority. One span per intent when the email is multi-issue, which is where a lot of quiet failures live.
- retrieve β the query issued, the chunks returned with their document IDs, versions, and scores, and the rerank step as its own child span.
- tool β one span per external call. The endpoint, the arguments, the status code, the latency, and a fingerprint of the response.
- generate β the model version, prompt template ID and version, token counts in and out, stop reason.
- validate β each guardrail as a child span with an individual pass or fail: PII check, forbidden-phrase check, numeric sanity, policy citation verification.
- decide β the terminal call. Send, draft for review, or escalate, with the reason attached.
Every span in the trace shares a trace ID, and that trace ID belongs on the outbound email's headers, on the helpdesk ticket, in the audit record, and in any alert that fires. One identifier connecting all of them turns "find me everything about this incident" into a single query instead of an afternoon.
Reason codes, and why confidence scores aren't enough
This is the piece most implementations are missing, and it's the one that pays for itself fastest.
A confidence score of 0.62 tells you the model was unsure. It does not tell you what it was unsure about, which means it gives you nothing to act on. Aggregate a quarter of confidence scores and you get a histogram. Aggregate a quarter of reason codes and you get a work plan.
A reason code is a value from a fixed, versioned enumeration recorded at every decision point. Not free text. The moment reasons are free text, two engineers write kb_gap and knowledge base gap and your aggregation is dead.
Escalation reason codes might include NO_RELEVANT_SOURCE, SOURCE_CONFLICT, LOW_CONFIDENCE_INTENT, TOOL_FAILURE, POLICY_HARD_RULE, SENTIMENT_THRESHOLD, MULTI_INTENT_PARTIAL, ACTION_NOT_PERMITTED, and CUSTOMER_REQUESTED_HUMAN. Guardrail blocks get their own set. Retrieval gets codes for empty result sets, low top-score, and high inter-chunk disagreement.
The value shows up the first time you group by code. A team told "escalations are up 8%" has no idea what to do. A team told "escalations are up 8%, and 71% of the increase is SOURCE_CONFLICT on billing intents" knows that two help articles are contradicting each other and can go find them before lunch.
Two rules make reason codes hold up. Version the enumeration, so that when you split a code into two next quarter, historical data is still interpretable. And record the code for the successful path too, not just failures. Knowing why the agent was confident enough to send is exactly what you need when the confident answer turns out to be wrong.
What goes on a span
Attributes are where a trace becomes useful or stays decorative. The failure mode is recording plenty of operational data and none of the semantic data that explains behaviour.
Every span carries the basics: trace ID, span ID, parent span ID, name, start timestamp in UTC, duration, status. Then the ones specific to this domain.
On retrieval spans, record the rewritten query alongside the original, and for each returned chunk the document ID, the document version, the chunk index, and the similarity score. Document version matters more than teams expect β half the "the AI gave wrong information" investigations we've seen end with the discovery that the agent retrieved an accurate answer from a KB article that was accurate at the time and has since been edited. Without the version pinned in the trace, that story is unrecoverable.
On generation spans, record the model identifier including the exact version string, the prompt template ID and version, temperature, token counts, and the stop reason. When a model provider silently ships a point update, the version string in your traces is the only thing that lets you correlate the behaviour change to a date.
On tool spans, record the endpoint, the arguments, the HTTP status, and something about the response shape. Not the full payload β a fingerprint, a row count, a set of returned field names. The distinction between "the OMS returned zero orders" and "the OMS returned an error" is invisible in a status code that both cases render as 200, and it is exactly the distinction that explains why the agent told the customer they had no orders.
On decision spans, record the reason code, the confidence value, the threshold in force at that moment, and the policy version. Thresholds get tuned. A ticket that escalated in March under a 0.80 threshold looks like an anomaly when you review it in June against a 0.68 threshold, unless the trace remembers what the rule was at the time.
Sampling, cost and the tickets you can't afford to drop
Full-fidelity tracing on every email is expensive. Generation spans in particular can carry prompt and completion text measured in kilobytes, and at 200,000 emails a month that adds up quickly.
Head-based sampling β decide at ingest whether to trace this one β is the standard answer and the wrong one here. It's a coin flip taken before you know whether the ticket is interesting, which guarantees you lose exactly the rare failures you most need.
Tail-based sampling makes far more sense for email. Buffer the trace, and at the terminal decision keep it or drop it based on what actually happened. Keep every escalation, every guardrail block, every tool failure, every ticket that later reopens, every low-confidence send. Sample the clean high-confidence resolutions at 5% or less. In a typical queue that keeps well under a quarter of traces while retaining nearly everything anyone will ever want to look at.
The latency budget is the other reason email is a friendlier domain than chat. Nobody expects a reply in 400 milliseconds. You can afford to buffer, to write synchronously where it matters, and to run instrumentation that a real-time voice agent could never carry.
Reconstructing a ticket, concretely
Back to the phone support claim. With the model above, the investigation runs like this.
Pull the trace by the ID on the outbound email. The tree shows classification as plan_entitlements at 0.91 confidence, which is fine. The retrieve span returns four chunks; the top-scoring one at 0.83 comes from document kb-4417, version 6, titled "Enterprise plan benefits." The generate span shows the claim about phone support traced back to that chunk. The validate spans all pass. The decide span records CONFIDENT_GROUNDED_ANSWER at 0.89, above the 0.85 send threshold.
So the agent behaved correctly at every step. Open kb-4417 version 6 and there it is: a line about priority phone support written for a pilot programme that ran for two quarters in 2024 and was never removed from the article.
The agent didn't hallucinate. It faithfully repeated a stale document, which is a completely different problem with a completely different fix β and one you'd never diagnose from the outbound email alone. Ten minutes, and the remediation is a content audit rather than a prompt change that would have done nothing.
That's the whole argument for this work. Not elegance. The difference between a fix and a guess.
Wiring it up
Use OpenTelemetry. It's the default in this space now, the SDKs are mature in every language you'd plausibly use, and it means your agent traces land in the same Datadog or Grafana or Honeycomb instance as the rest of your infrastructure. Correlating an agent latency spike with a database incident is worth a great deal on the day you need it.
The semantic conventions for GenAI spans have stabilised enough to be worth adopting for model calls, token counts, and tool invocations. They cover none of the support-specific vocabulary β intent, reason code, KB document version, escalation target β so you'll define those yourself. Namespace them consistently (support.intent, support.reason_code, support.kb.doc_version) and write the schema down somewhere people will find it, because the second team to instrument something will otherwise invent a parallel vocabulary.
Then there's PII, which needs deciding before the first span is written rather than after. Email bodies are full of names, addresses, order numbers, and occasionally payment details, and prompt text on a generation span contains all of it. Redact at the SDK layer with consistent tokens so threads stay traceable, keep raw content in the audit store where retention and access controls are built for it, and make trace access a permission rather than a default. A trace backend that quietly became your most exposed copy of customer data is a bad surprise to have during a security review.
What traces won't tell you
Observability explains mechanism. It does not supply judgement.
A trace will show you that the agent retrieved chunk 3 and generated a reply grounded in it. It will not tell you the reply was condescending, or that a warmer opening would have prevented the escalation. Tone lives outside the trace, and the only reliable detector is still a human reading the actual emails.
Traces are also single-ticket by nature. A pattern spread across 400 tickets is a metrics problem, not a tracing one, which is why observability supplements the analytics layer rather than replacing it. You need aggregate dashboards to notice, and traces to explain.
And instrumentation has a real failure mode of its own: recording enormously and querying never. A trace store nobody opens is an expensive write-only database. If your team hasn't pulled a trace in a month, either your agent is remarkably well-behaved or your traces aren't answering the questions people actually have. In our experience it's usually the second.
How Robylon instruments this
Robylon emits a full trace for every inbound email, with the span structure described above and a versioned reason-code enumeration covering escalation, guardrail blocks, retrieval outcomes, and terminal decisions. Any ticket can be reconstructed end to end from the support console β retrieved sources with document versions, tool calls with arguments and responses, the confidence value and the threshold that was in force, and the reason code behind the outcome.
Reason codes feed the operational loop rather than sitting in a log. They drive the escalation handoff context agents see when a ticket reaches them, they surface knowledge gaps ranked by ticket volume, and failures tagged with a code become candidates for the regression suite so the same root cause doesn't recur after the next model upgrade. Traces are tail-sampled with all escalations, guardrail blocks, and reopened tickets retained in full, and redacted at capture with the unredacted record living in the separate audit store.
The reason this matters commercially rather than just architecturally: 60β80% autonomous resolution is a number a buyer has to trust, and trust comes from being able to open any single ticket in that 80% and see exactly why it resolved the way it did. The email platform overview shows how the tracing layer surfaces in the product.
Ready to answer "why did the AI say that" in minutes instead of never? Robylon AI resolves 60β80% of customer emails autonomously, with full per-ticket traces and reason codes across Zendesk, Freshdesk, Shopify, Stripe, and 60+ other integrations. Explore the email platform at robylon.ai
FAQs
What is the difference between observability and monitoring for AI agents?
Monitoring answers questions you decided on in advance β containment rate, escalation volume, latency β and shows them on a dashboard. Observability lets you ask questions nobody anticipated, like why one specific ticket resolved the way it did or which retrieved chunk produced a particular claim. Monitoring tells you something changed on Tuesday; observability tells you what caused it. Aggregate metrics have already discarded the per-event detail needed to answer the second kind of question, so you need both layers rather than one.
What should a trace for an AI email agent contain?
One trace per inbound email through to a terminal outcome, with nested spans for ingest, classify, retrieve, tool calls, generate, validate, and decide. Retrieval spans need document IDs, versions, and similarity scores. Generation spans need the exact model version and prompt template version. Tool spans need endpoint, arguments, status, and response shape. Decision spans need the reason code, the confidence value, and the threshold in force at that moment, since thresholds get tuned and old tickets look anomalous without it.
Why are reason codes better than confidence scores?
A confidence score of 0.62 says the model was unsure but not what it was unsure about, which gives you nothing to act on. A reason code from a fixed, versioned enumeration names the cause β no relevant source found, conflicting sources, tool failure, hard policy rule, sentiment threshold. The difference shows up on aggregation: "escalations up 8%" is unactionable, while "escalations up 8%, mostly source conflicts on billing" points at two contradictory help articles you can fix today. Never use free text; it destroys aggregation immediately.
Is an audit trail the same as agent observability?
No, and conflating them causes trouble. Audit trails serve regulators: multi-year retention, tamper-evident storage, complete coverage, optimised for proving what happened. Traces serve engineers: days to weeks of retention, sampled, optimised for fast querying and root-cause work. They record overlapping information for entirely different purposes and have incompatible requirements around retention cost and mutability. Build both, keep raw customer content in the audit store, and redact traces at capture.
How do you control the cost of tracing an AI email agent?
Use tail-based sampling rather than head-based. Deciding at ingest whether to trace a ticket is a coin flip taken before you know whether it's interesting, which loses exactly the rare failures you need. Instead, buffer the trace and decide at the terminal outcome: keep every escalation, guardrail block, tool failure, low-confidence send, and later-reopened ticket, then sample clean high-confidence resolutions at 5% or less. Email's relaxed latency budget makes this buffering practical in a way it isn't for voice.

.png)

.png)
