Error handling for automation
The envelope, the exit classes, and which failures are worth retrying.
Written By Dustin
Last updated 24 minutes ago
Every failure carries a TVO-<AREA>-<NNN> code. Branch on the code, never on the message text — the text is presentation and may be reworded; the code is a versioned contract.
The envelope
Under --json, and across the SDK and MCP edges, a failure is the same object: code, title, cause, exit_code, and a context map. Success uses a top-level result; failure uses error. Nothing else goes on stdout in JSON mode — warnings, hints, and deprecation notices are always stderr, so a parser never has to strip prose out of a document.
Exit classes
Exit codes are coarse — one per area class — so a script can branch on category cheaply while the precise code lives in the JSON.
Two exits will catch you out. Creating a conflict exits
0— a conflict is a first-class object and a successful outcome, not a failure — so automation that treats zero as "nothing to look at" will silently build on top of conflicts. ChecklistConflictsorstatus, not just the exit code. And exit12is the inverse: the operation succeeded and is durable, and only a post-event plugin failed. Re-running a land that exited 12 re-runs work that already committed.
The error catalog in the documentation is the system of record for every code and its exit class. This table is the shape, not the register.
Usually worth retrying
Never worth retrying
Errors are values in the SDK
The engine returns denials, it does not throw them, so a buggy or compromised edge cannot suppress one by swallowing an exception. Every SDK operation returns { ok: true, … } | { ok: false, error } and TypeScript forces the check.
const read = client.readFile("src/secret.rs");if (!read.ok) { if (read.error.code === "TVO-PERM-001") { /* no clearance — ask a human */ } return;}unwrap(outcome) is the scripting shortcut: the payload, or a thrown TovioError carrying the envelope unmodified. Only construction sites throw — connect (a refused token yields no client) and openWriteSession. A malformed envelope degrades to TVO-CORE-000, never to an empty object, so error.code is always present.
Handle expiry deliberately
Long-running automation should renew before expiry rather than catching the failure: tovio agent renew <token_id> --expires-in <hours>. That is a re-issue, not an extension — the old token's window is unchanged.
Never log the token
Log the code and the operation. Not the credential, not the path scope of a protected read, not the contents of an error context map you have not inspected.