Skip to main content
Faithful to the live Deck API v2.0.0 (captured 2026-06-09). The object shapes, event names, error envelope, IDs and formats below are exactly what real Deck returns — this page is the vocabulary to assert against, not a tutorial on Deck. If you know the API, the only new surface is the control layer at the bottom.
npx prod-break run deck
# url=http://localhost:8801   key=pbw_3f9a…   pack=deck@0.1.0
# → point your Deck client's base URL at the url; everything else is unchanged
Deck ships no SDK and no sandbox, so your app already points a plain HTTP client at https://api.deck.co/v2 — repointing it is a one-line base-URL change (Interception).

Objects

agent ──owns──▶ task ──run──▶ task_run ──in──▶ session
                 │               │
        input/output schema   credential ─▶ source (a website)

                          event ─▶ event_destination
{
  "id": "task_P8TAk2", "object": "task", "agent_id": "agt_TkKr",
  "name": "Post item on Marketplace",
  "status": "live",                 // learning | test | live
  "input_schema":  { "type": "object", "properties": { "title": { "type": "string" }, "price": { "type": "number" } } },
  "output_schema": { "type": "object", "properties": { "listing_id": { "type": "string" }, "listing_url": { "type": "string" } } }
}

Events

object.action. data is a frozen subset snapshot, and it differs from the REST view: empty fields serialize as "" (not null) and some fields are dropped — workflow_id is the canonical divergence (null over REST, "" in the event).
ResourceEvent types
agentcreated · updated · deleted
sourcecreated · updated · deleted
credentialcreated · unverified · verified · invalid · deleted
sessionqueued · running · idle · completing · completed · failed
taskcreated · updated · reset · deleted
task_runqueued · running · interaction_required · review_required · completed · failed · canceling · canceled
storagecreated
event_destinationactivated · deactivated
usercreated
task_run.**canceling** is spelled with one L in the event name, while the run status is cancelling (two L). /event-types is the only unpaginated list. Deliveries are signed with the Standard Webhooks scheme (webhook-id / webhook-timestamp / webhook-signature).

Errors

The { type, code, message, field? } element is the same everywhere, but it surfaces in two places — which one you get depends on whether the call failed or the run failed. 1 · A call fails → HTTP error response. Status 401/403/404/409/422/429/500, body is the envelope:
// e.g. GET /tasks/task_xxx with a bad key  → HTTP 401
{ "errors": [ { "type": "request", "code": "api_key_invalid", "message": "The API key is not valid.", "field": null } ],
  "request_id": "req_v0hE8IKbqoap" }
codeHTTP
request / api_key_invalid401
request / task_not_found (any *_not_found)404
idempotency / idempotency_error409
rate_limit / rate_limit_exceeded429
400 validation errors are the one exception — a different, RFC-9110-style shape (not the envelope):
{ "type": "…", "title": "Invalid input", "status": 400,
  "errors": { "input.price": ["must be a number"] }, "traceId": "…" }
Quirks: an unknown route returns 404 with an empty body (no envelope); a bad filter value or over-range Limit is lenient → 200, never 400. 2 · A run fails → the call succeeds (HTTP 200), the error lives inside the run. result: "failure", output: null, and errors[] holds the same element:
// GET /task-runs/trun_8a2  → HTTP 200
{ "id": "trun_8a2", "status": "failed", "result": "failure", "output": null,
  "errors": [ { "type": "task", "code": "blocked", "message": "Site blocked automated access." } ] }
codeWhen the run fails because…
task / blockedthe site blocked automated access
task / task_failedthe run failed
task / timeoutit exceeded the 30-min run timeout
task / task_result_unknownthe outcome couldn’t be determined
auth / auth_invalidthe credentials were rejected by the source
source / source_blocked · source_not_availablethe source itself failed
type taxonomy (full set): api · auth · idempotency · interaction · organization · rate_limit · request · session · source · storage · task. Note api_key_invalid is type request, not authauth is only for credential-vs-source failures.

Pinnable values

The fields that originate outside Deck’s logic — the only ones you supply. Everything else (status, result, runtime_ms, ids, timestamps) is engine-derived and unfakeable.
PathWhatBinds atnull when
outputrun result body (shape = task’s output_schema)completedrun fails / cancels
output.<field>one output fieldcompletedagent couldn’t fill it
errorsfailed run’s errors[]failedsuccess arm → []
interactionmid-run challenge (mfa / security_question / account_selection)interaction_requirednot paused → null
resultsuccess / failure / unknownterminalnon-terminal → null

Formats & conventions

IDslowercase prefix + 16-char base62 suffix (trun_Ijz17mHzBrps2bt0), immutable
Credentialsonly username reads back; secrets are write-only
Pagination{ data[], has_more, next_cursor, request_id }; param Limit (capital L), clamped at 100
AuthAuthorization: Bearer sk_live_… (sandbox: the printed pbw_… key)
Content-Typeapplication/json; charset=utf-8

Control (prod-break)

The only surface that isn’t real Deck. Everything above is the vendor’s; this is how you make a given outcome happen on demand.
// SETUP — seed your create-once definitions (agents/tasks/sources/credentials); runs reference them by id.
await sandbox.seed("tasks", [{ id: "task_P8TAk2", agent_id: "agt_TkKr", status: "live",
  output_schema: { type: "object", properties: { listing_id: { type: "string" }, listing_url: { type: "string" } } } }]);

// pin an out-of-control value, or force the failure arm
await sandbox.exogenous.on("task_run", "output", { listing_id: "lst_77c", listing_url: "https://…/lst_77c" });
await sandbox.exogenous.on("task_run", "branch", "failed");

// trigger a world event (the site intervened); the matching event + run fields fall out
await sandbox.world.trigger("task_run.interaction_required", { task_run_id: "trun_8a2", interaction: { type: "mfa", /* … */ } });

// arm an inbound API fault — your call gets Deck's real envelope above
await sandbox.faults.arm("task_runs", "create", { status: 401, code: "api_key_invalid" });

// advance the virtual clock — due lifecycle events drain in order
await sandbox.clock.advance("60s");
  • Event names and behavior are pack-declared — the engine is generic; the Deck vocabulary above lives in this pack.
  • Control calls hit /__admin__/*. Their errors (e.g. a task_run_id that doesn’t exist → 409) go to your test, not the app under test, and are not Deck’s error envelope. sandbox.world.next() lists the world events whose preconditions currently hold.
  • No magic test inputs — Deck has none; force outcomes through this surface, never reserved input values.
  • Concepts: exogenous values · force a branch · world events · the clock · HTTP API.
Pinned against Deck API v2.0.0 (2026-06-09), guarded by contract tests that diff the pack against captured live responses.