A returned request that arrives without its history costs more than a request that was never automated. The human has to reconstruct the customer's goal, re-verify identity, re-run diagnostics, and guess which actions already took effect.
A usable handoff record separates four kinds of content that are easy to blur together (Salted CX):
- What the requester said, in their own words, including the original ask and any goals stated along the way.
- What the system verified, with a reference (account or order ID, verification status, channel, language).
- What the system inferred, marked as inference, not fact.
- What the system did, with a confirmed, failed, or unknown status for each consequential action, and the reference needed to check it.
That last point carries the most risk. A timeout means the result is unknown until checked, not necessarily that nothing happened, and retries must not create a second refund, replacement, or ticket (Salted CX). If your record does not carry a deduplication key or an action ledger, your retry logic is a duplication engine.
Here is a schema that holds up under audit. It borrows the shape of a trace, where a root span carries the whole operation and child spans carry units of work that share one trace identifier (OpenTelemetry).
{
"handoff_id": "ho_9c31",
"trace_id": "5b8aa5a2d2c872e8321cf37308d69df2",
"original_request": "customer asks for refund on order 8814",
"requester_text_ref": "conversation://c_7712/turn/3",
"escalation_trigger": {
"kind": "permission_boundary",
"detail": "refund exceeds agent approval limit",
"rule_ref": "policy://refunds/v4"
},
"actions_taken": [
{"action": "lookup_order", "status": "confirmed", "ref": "ord_8814"},
{"action": "issue_refund", "status": "not_attempted", "ref": null}
],
"ownership": {
"queue": "billing_returns",
"requested_at": "2026-01-14T09:12:04Z",
"accepted_by": null,
"accepted_at": null
},
"resolution": {"state": "open", "closed_by": null, "reason_code": null}
}
Two details matter more than the field names. First, escalation_trigger names a rule, not a sentiment. A model's self-reported confidence should not be the trigger for a human request or a permission boundary without validation on that specific workflow (Salted CX). Second, resolution.reason_code is populated even when the outcome is "closed, no action needed." A closure without a reason code is indistinguishable from a drop.
On the telemetry side, prefer attributes on an existing span over creating new spans for every small fact. Attributes are the cheapest place to accumulate data on a single event and give you the correlation power to answer "what is different about the requests that failed," while a new span is warranted for a real unit of work, a network boundary, or an async boundary (Honeycomb). The handoff itself is a boundary between an automated path and a human path, so it earns a span. The trigger kind, the policy reference, and the queue name are attributes on it.