05 The rejection record
What a rejection has to carry to be worth reading.
A rejection that says “error” tells the next reader nothing, and the next reader is usually a retry loop. A record is what turns a rejection into a decision somebody else can make.
A record from the worst case in the kit
The refund timed out after the money moved, and the provider’s status endpoint is
down. This is what the corrected design wrote.
{
"record_id": "task-CASE-7731-R02",
"task_id": "task-CASE-7731",
"attempt": 2,
"timestamp": "2026-09-16T09:00:00+05:30",
"stage": "action.refund",
"failure_class": "uncertain_action",
"detected_by": { "type": "rule", "name": "reconciliation" },
"reason": "the action may have executed and provider state cannot be read",
"missing_evidence": [],
"side_effects": [
{
"action": "refund",
"target": "payment-provider",
"idempotency_key": "task-CASE-7731:refund",
"state": "unknown"
}
],
"permitted_next_actions": [
"escalate with the idempotency key so a human can reconcile"
],
"prohibited_actions": [
"re-issue the action",
"report the refund as failed",
"report the refund as complete"
],
"retry": {
"attempts_used": 1,
"max_attempts": 8,
"deadline": "2026-09-16T09:02:00+05:30"
},
"next_step": "escalate",
"owner": "refunds-duty-officer",
"escalation": {
"queue": "refunds-manual-review",
"reason": "refund outcome unknown and the provider status endpoint is unavailable; reconcile by idempotency key before any re-issue",
"respond_by": "2026-09-16T10:00:00+05:30"
}
}
- The three prohibitions are the useful part. Two of them forbid reporting an outcome, in either direction, because both would be a guess.
- The idempotency key travels with the record. Without it the person who picks this up has nothing to reconcile against.
- state is “unknown”, not “failed”. The money did move. The system simply cannot see that yet, and says so.