Operations reference
Retrieve what happened
An operation follows consequential work from request to result. Use the ID from your authenticated workflow to inspect the saved record. Receipt and story are separate reads: neither promises an outside-world result that was not observed.
How do I find an operation?
List operations in your authenticated workspace. Read a single operation to see its recorded state; then follow its receipt and story links to inspect evidence.
GET /api/v1/operations?limit=20&offset=0
GET /api/v1/operations?status=needs_a_personThe list is newest first, with a total. status narrows it, its total too, to one group: in_progress, needs_a_person (waiting for a decision, or blocked), finished or stopped (failed or cancelled). Any other value is 400 invalid_status. Each item has operation_id, state, outcome, terminal, next_action, mission_name, created_at and duration_ms (0 until it finishes), and sample on the “See it work” sample.
GET /api/v1/operations/{id}The detail is the recorded operation and what it is directly linked to. Nothing in it is guessed from timestamps or text.
stateis where the work is;terminalis true once it can’t change;outcomeis what the record alone supports (observed,failed,cancelledorunknown), never outside confirmation: that is the receipt’s.next_actionsays what to do:await_human_decision,review_blocker,review_failure,inspect_evidenceorwait_for_update.created_atandupdated_atare when it was admitted and last changed.mission_id,conversation_id,trace_idandroutine(recipe_id,kind,version) are what it is linked to, or null. The top-levelrecipe_idis the same asroutine.recipe_id, kept for older clients.decision_idsare the decisions people made about it, oldest first (GET /api/v1/cadreen/decisions/records/{id}). The decisions it is waiting on are atGET /api/v1/cadreen/decisions?linked_operation_id={id}.held_callsare the tool calls its mission held for a person, always an array. Each hastool_name(the tool’s own display name),target(where the call would go: a host, a recipient; empty when the tool names none; never an argument value),decision_id(the decision that holds it) andstate(heldwhile it waits for a person). It lists the oldest 20;held_calls_moreis true when the mission held more than that, and the list is then not all of them.receiptandstoryare links (href) with anavailability:available,empty(it finished with no linked evidence) orpending(it is still running with none yet).sampleis true only on the “See it work” sample, whose approval sent nothing.
How do I hear when one changes?
Read the change feed. Every state change of your workspace’s operations comes once, oldest first, and none is skipped. Keep next_cursor and read again; wait (up to 30 seconds) holds the request until something changes. To follow only what happens from now, ask for start=now first, then read the list, then the feed from that cursor: nothing is missed and no history is read. A 410 means the cursor can’t be read from any more: read the list again, then start over.
GET /api/v1/operations/changes?start=now
GET /api/v1/operations/changes?after={next_cursor}&wait=25What does the receipt show?
The receipt answers what the recorded evidence supports. The server checks caller access; unknown and foreign IDs return the same not-found result. A completed internal operation is not confirmation from an outside service.
GET /api/v1/operations/{operation_id}/receipt
Authorization: Bearer sk_cadreen_...
Accept: application/json{
"operation_id": "7c418e81-5a56-4d28-9a29-0fc9258a09e9",
"intent": "Draft a reply for review",
"state": "completed",
"created_at": "2026-10-01T10:00:00Z",
"evidence": [
{
"source": "operation",
"provider": "cadreen",
"action": "complete",
"status": "completed",
"level": "observed",
"detail": "The recorded operation completed.",
"timestamp": "2026-10-01T10:01:00Z"
}
],
"overall_level": "observed",
"summary": "The work you asked for is done. Allowed by your rule: Drafts for review. Cadreen saw it finish. No outside service was involved.",
"cost": { "usd": "0.04784", "surface": "cadreen", "model_calls": 3 },
"allowed_by_rule": { "id": "drafts-for-review", "name": "Drafts for review" }
}What if it cannot finish?
A request can ask for clarification, be blocked by policy, or wait for a person. Do not treat any of these states as success. A missing or inaccessible operation returns not found; if the workspace cannot be verified, the server reports that availability problem instead of exposing another workspace’s work.
How did the work happen?
The story orders only directly linked records. Missing evidence stays missing; it is not reconstructed from matching timestamps, text, or trace IDs.
GET /api/v1/operations/{operation_id}/story
Authorization: Bearer sk_cadreen_...
Accept: application/json{
"operation_id": "7c418e81-5a56-4d28-9a29-0fc9258a09e9",
"state": "completed",
"summary": "The work you asked for is done. Ada approved the next step. The approved action ran once.",
"beats": [
{
"boundary": "model",
"beat": "asked",
"text": "You asked: \"Email Ada the invoice\"",
"level": "observed",
"timestamp": "2026-10-01T10:00:00Z",
"refs": {}
},
{
"boundary": "tool",
"beat": "held_for_approval",
"text": "Cadreen held “Email Ada the invoice” for a person to approve before it could run.",
"level": "observed",
"timestamp": "2026-10-01T10:00:01Z",
"refs": {}
},
{
"boundary": "human",
"beat": "human_approved",
"text": "Ada approved the next step.",
"level": "observed",
"timestamp": "2026-10-01T10:00:02Z",
"refs": {
"decision_id": "723a8208-5df7-49be-8c47-a0b934cb6972",
"decision_record_id": "4f4e470d-7f56-4217-87f8-e95ac5180707"
},
"decision_outcome": "approved"
},
{
"boundary": "tool",
"beat": "tool_ran",
"text": "“Email Ada the invoice” was approved, ran once and finished.",
"level": "observed",
"timestamp": "2026-10-01T10:00:03Z",
"refs": {}
}
],
"reusable_rule": null,
"reversal_consequences": null
}Each beat is one step the work took. boundary says where it happened: model, tool, provider, human, money, network, physical or learning. beat says what happened there, and its text is a whole sentence you can show as it is. The list of beats can grow, so show a beat you don’t know by its text and don’t drop it. The confirmed beat belongs to money only: it appears when a payment’s own provider proof says it settled.
A story reads a bounded number of rows from each part of an operation’s record: the latest ones, shown oldest first (suggested improvements keep their start). When a part was longer than that, the story carries more: a list with one entry per cut part, each with its source (attempts, outbox, decisions, settlements, device_commands, plan_questions, system_pauses or routine_proposals), how many rows are shown, and a sentence in text to show as it is. It is absent when nothing was cut, so a story without it is the whole record. When payments, decisions, questions or pauses were cut, the summary does not count them or name a payment’s final state, and says it shows only part of what was recorded.
Why did it fail?
When a job fails for a cause Cadreen knows, the story carries two more fields, and the summary and the stopped beat say the same thing in one sentence. cause is the cause: model_not_configured, model_retired, model_key_missing, platform_model_key_refused, platform_model_out_of_credit, model_key_refused, model_out_of_credit, model_provider_busy, model_provider_unavailable or tool_circuit_open. cause_owner says whose it is: customer_setup (something you set up and can change), provider (an outside service failed) or cadreen (Cadreen’s own setup, so there is nothing for you to do). Both are there together or both are left out. A job that failed for any other reason has neither.
{
"operation_id": "3b9e1c52-0d7a-4f0e-8a64-5c2d7e9f1a30",
"state": "failed",
"summary": "The AI provider didn't accept your key, so this couldn't run. Replace the key on AI keys and budgets, then try again.",
"beats": [
{
"boundary": "model",
"beat": "asked",
"text": "You asked: \"Email Ada the invoice\"",
"level": "observed",
"timestamp": "2026-10-01T10:00:00Z",
"refs": {}
},
{
"boundary": "network",
"beat": "stopped",
"text": "Cadreen stopped: The AI provider didn't accept your key, so this couldn't run. Replace the key on AI keys and budgets, then try again.",
"level": "failed",
"timestamp": "2026-10-01T10:00:01Z",
"refs": {}
}
],
"reusable_rule": null,
"reversal_consequences": null,
"cause": "model_key_refused",
"cause_owner": "customer_setup"
}What happened, and what changed state?
The story’s sibling route answers a narrower question: what happened to this job, and why. It returns the same state and summary as the story, the cause as an object (code, owner and one sentence) when the job failed for one, and steps, each change of state, oldest first, with by saying what kind of actor made it: person, edge_device, rule, agent or system. It returns only codes and fixed sentences, never a recorded message. The same people can read it who can read the story. A workspace admin or the owner, signed in as a person, also gets errors (your organization’s recorded error codes for the job) and audit (the audit entries about it); everyone else, an API key included, gets the rest, and admin says which you got.
GET /api/v1/operations/{operation_id}/what-happened
Authorization: Bearer <a signed-in person's token>
Accept: application/json{
"operation_id": "3b9e1c52-0d7a-4f0e-8a64-5c2d7e9f1a30",
"state": "failed",
"summary": "The AI provider didn't accept your key, so this couldn't run. Replace the key on AI keys and budgets, then try again.",
"cause": {
"code": "model_key_refused",
"owner": "customer_setup",
"sentence": "The AI provider didn't accept your key, so this couldn't run. Replace the key on AI keys and budgets, then try again."
},
"steps": [
{
"from": "admitted",
"to": "failed",
"at": "2026-10-01T10:00:01Z",
"by": "system",
"cause": { "code": "model_key_refused", "owner": "customer_setup", "sentence": "..." }
}
],
"admin": false
}What if the read fails?
Both routes answer in the same error envelope, with the code in error.code:
404 operation_not_found: no operation with that ID exists for your account. It is the same answer for another organization’s job, another workspace’s job, and a conversation’s job you may not view, so the answer never tells you which.400 invalid_operation_id: the ID is not a UUID.401 unauthorized: the request isn’t tied to an organization.503 what_happened_unavailable: only on/what-happened, when it can’t be read right now. Try again in a minute.
{
"error": {
"type": "...",
"code": "operation_not_found",
"message": "No operation with that id was found for your account",
"next": "...",
"doc_url": "..."
}
}How do I prove what happened to someone outside?
Issue a signed receipt. It is a document your workspace signs with its own key, which anyone can check offline with this deployment’s public key from GET /.well-known/cadreen/receipt-roots.json, without an account. Here is how to check one, in a browser, on the command line or in your own code. It says what was asked, who allowed it, what happened and how strongly that is supported, and what is still not known, at the same levels as the receipt above.
Only what is safe to share leaves your workspace. The levels, what each rests on (basis), the steps, the outcome and the times are in it. The request’s words, its limits, the rule’s name, providers, people’s names and the operation’s ID are kept private, and the receipt lists each one it kept private. You can keep more private with withhold, never less.
A person in the workspace issues it; an API key can’t yet. Each receipt is kept exactly as it was issued: issuing again for the same audience makes a new one that supersedes it; each audience has its own line of receipts. If this deployment has no issuer key, issuing answers 503 receipt_signing_not_configured, and the receipt above keeps working.
POST /api/v1/operations/{operation_id}/receipts
Authorization: Bearer <a signed-in person's token>
Content-Type: application/json
{ "audience": "counterparty", "withhold": ["actor.role"] }{
"receipt_id": "rcp_01jbz6m3q4x2c9v8b6n5k7h2wd",
"sequence": 1,
"supersedes": null,
"document": {
"format": "cadreen.receipt",
"format_version": "1",
"body": {
"issuer": { "deployment": "iss_cadreen_hosted_1", "display_name": "Acme", "verified_domain": null, "workspace_key_id": "rk1_5d5prcyn6c526ml6" },
"operation": { "ref": "opr_...", "state": "completed", "terminal": true, "created_at": "...", "updated_at": "..." },
"evidence": [{ "ref": "e1", "source": "settlement", "level": "inferred", "basis": "provider_accepted", "counts_toward_outcome": true, "at": "..." }],
"outcome": { "level": "inferred", "intended_result": "not_known_yet", "next": "waiting_for_provider" },
"unknowns": [{ "about": "finality", "evidence_ref": "e1", "since": "...", "expected_by": null }],
"withheld": [{ "path": "/request/statement", "class": "request.text", "commitment": null }]
},
"signature": { "alg": "Ed25519", "key_id": "rk1_5d5prcyn6c526ml6", "value": "..." },
"key_certificate": { "body": { "...": "..." }, "signature": { "...": "..." } }
}
}GET /api/v1/operations/{operation_id}/receipts
GET /api/v1/receipts/{receipt_id}What should I rely on?
overall_levelis the receipt’s conservative summary. Values include confirmed, observed, inferred, unknown, failed, and cancelled.evidenceexplains each linked source and when it was recorded.costis what the operation’s model calls cost:usdas an exact decimal string, thesurfaceit was charged to, andmodel_calls. It is absent when no charged call names the operation, including work that ran before calls were recorded against operations.allowed_by_rulenames the rule of your workspace that allowed the work by itself, with nobody asked: itsidand thenameit had when it decided. The summary says the same in words (“Allowed by your rule: Drafts for review.”), and the story’spolicy_allowedbeat carries it asrule. It is absent when no rule is recorded: nothing held the work, a person approved it, or it was decided before rules were recorded. Cadreen never guesses which rule it was.basison each evidence line says what its level rests on. Only a provider’s own proof (its status and transaction hash, a signed notification, chain finality, fresh device telemetry) can beconfirmed. Cadreen’s own record, a ledger entry and a person’s answer are at mostobserved. What your own software reported is at mostinferred.- A signed receipt proves that this Cadreen, for your workspace, said exactly this. It does not prove the content true against a compromised Cadreen.
beatsare an ordered account, not a claim that every system boundary has evidence.- A missing receipt/story is not proof that no work happened. It means this read is unavailable or the operation could not be found for your account.