Skip to main content

Exceptions

A queue of failures and anomalies surfaced as first-class, triageable items rather than log lines.

Overview

The platform files an exception whenever something needs a human's attention — an orchestration run that failed after exhausting retries, a guardrail tripwire that aborted an action, or an approval that expired without a decision. Each item carries a severity, structured detail, and provenance links, and moves through an open → acknowledged → resolved triage lifecycle. Repeated identical failures fold into one item with an occurrence count, so a hot failure loop never floods the queue.

Exceptions are auto-filed by the platform (or filed explicitly as manual); there is no public create endpoint. They are read, acknowledged, and resolved through the API, and an exceptions.created webhook fires on the first occurrence so alerting is push, not poll.

See the Permissions Reference for the IAM action strings for this module.

Data Model

ExceptionItem

FieldTypeDescription
idstringPublic ID, exc_ prefix
project_idstringOwning project
statusstringopen, acknowledged, resolved
severitystringinfo, warning, critical
kindstringrun_failed, guardrail_tripwire, approval_expired, quota_unpriced, manual
titlestringHuman-readable one-line summary
detailobject | nullStructured context (tool, error, guardrail version)
occurrence_countintegerTimes this exact failure was observed while open
last_seen_atstringTimestamp of the most recent occurrence
run_idstring | nullOriginating orchestration run
node_idstring | nullOriginating node id within the run's graph
agent_idstring | nullAssociated agent
guardrail_versionstring | null<guardrailId>@<version> for a guardrail_tripwire item
acknowledged_bystring | nullAcknowledging user's public ID
resolved_bystring | nullResolving user's public ID
resolution_notestring | nullOptional note recorded at resolution
created_at / updated_atstringTimestamps

Key Concepts

Severity

Severity is keyed to actionability, not raw "badness". Each kind has a default a producer can override:

KindDefault severityWhy
run_failedcriticalA run died after exhausting retries — needs intervention
guardrail_tripwirewarningThe guard worked as designed; also a feedback-loop signal
approval_expiredwarningFail-safe missed SLA — the action never ran
quota_unpricedwarningA cost cap is protecting nothing; needs a config fix, not incident response
manualwarningAuthor-chosen

Occurrence dedup

Repeated identical failures fold into one open item rather than filing duplicates: a partial unique index keys at most one open exception per dedup key, and each recurrence bumps occurrence_count and last_seen_at (only the first emits exceptions.created). A resolved item frees the key, so a recurrence after resolution opens a fresh exception. manual items are never deduped.

Triage lifecycle

An item is open when filed. Acknowledge it (acknowledged) to signal someone is on it — distinct from resolve (resolved, "fixed"), which records the resolver and an optional note. A resolved item is terminal: acknowledging or resolving it again returns 409 EXCEPTION_ALREADY_RESOLVED.

Producers

Exceptions are filed by subscribing to platform events, so producers stay decoupled: run_failed rides the existing orchestration_runs.failed event, approval_expired rides approvals.expired, and guardrail_tripwire rides a dedicated guardrail.tripwire event emitted from the guardrail dispatch path. Every filing is fire-and-forget — it never disturbs the producer.

quota_unpriced is the exception to the event-driven pattern: it is filed inline from the quota pre-generation check, which is the only place that knows a cost cap just evaluated against an unpriced window. It is deduped on the quota rather than the window, so one dead cap is one triage item and occurrence_count reads as the number of generations that ran unprotected. The check fails open, so a filing error can never block a generation.

Examples

# List open, critical exceptions in a project
soat list-exceptions --project-id proj_01 --status open --severity critical

# Triage one
soat acknowledge-exception --exception-id exc_01
soat resolve-exception --exception-id exc_01 --note "Root cause fixed; reran the pipeline."