<!-- Generated by `just docs` from proto/automaton/v1/jobs.proto. Edit the source, not this file. -->

# JobsService

Hand over work that is bigger than one call: analyze ten thousand rows, move forty thousand files, write to a hundred records. You submit a plan and read receipts; the rows themselves stay on our side and never travel back to you. A job outlives the request that started it, so it is submitted once and read back later.

Every call is a POST to `https://api.atmon.ai/automaton.v1.JobsService/<Call>` with a JSON body, and authenticates with `Authorization: Bearer <your project key>`. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention.

## Calls

| Call | Request | Response | Summary |
| --- | --- | --- | --- |
| `SubmitJob` | `SubmitJobRequest` | `SubmitJobResponse` |  |
| `GetJob` | `GetJobRequest` | `GetJobResponse` |  |
| `ListJobs` | `ListJobsRequest` | `ListJobsResponse` |  |
| `ListJobEvents` | `ListJobEventsRequest` | `ListJobEventsResponse` | ListJobEvents reads one job's log in sequence order, each entry naming the chain that caused it. |
| `AnswerAsk` | `AnswerAskRequest` | `AnswerAskResponse` | AnswerAsk is how a person answers a question a job addressed to them. |
| `ListAsks` | `ListAsksRequest` | `ListAsksResponse` | ListAsks reads the questions a project has addressed to people: what is outstanding, who holds it, what was answered and by whom. |
| `GetAsk` | `GetAskRequest` | `GetAskResponse` | GetAsk reads one of them. |

### SubmitJob

Request `SubmitJobRequest`, response `SubmitJobResponse`.

```http
POST /automaton.v1.JobsService/SubmitJob HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "programJson": "{}",
  "idempotencyKey": "...",
  "mode": "...",
  "simulateFirst": true
}
```

The response:

```json
{
  "job": {
    "id": "...",
    "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
    "programJson": "{}",
    "programHash": "...",
    "estimate": { ... },
    "planApprovalId": "...",
    "park": { ... },
    "stepReceipts": [{ ... }],
    "receipt": { ... },
    "lastError": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "updatedAt": "2026-01-31T09:15:00Z",
    "finishedAt": "2026-01-31T09:15:00Z",
    "pendingAsk": { ... },
    "saga": { ... },
    "mode": "...",
    "simulatedFrom": "...",
    "skillSlug": "..."
  }
}
```

### GetJob

Request `GetJobRequest`, response `GetJobResponse`.

```http
POST /automaton.v1.JobsService/GetJob HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "id": "..."
}
```

The response:

```json
{
  "job": {
    "id": "...",
    "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
    "programJson": "{}",
    "programHash": "...",
    "estimate": { ... },
    "planApprovalId": "...",
    "park": { ... },
    "stepReceipts": [{ ... }],
    "receipt": { ... },
    "lastError": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "updatedAt": "2026-01-31T09:15:00Z",
    "finishedAt": "2026-01-31T09:15:00Z",
    "pendingAsk": { ... },
    "saga": { ... },
    "mode": "...",
    "simulatedFrom": "...",
    "skillSlug": "..."
  },
  "simulation": {
    "id": "...",
    "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
    "programJson": "{}",
    "programHash": "...",
    "estimate": { ... },
    "planApprovalId": "...",
    "park": { ... },
    "stepReceipts": [{ ... }],
    "receipt": { ... },
    "lastError": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "updatedAt": "2026-01-31T09:15:00Z",
    "finishedAt": "2026-01-31T09:15:00Z",
    "pendingAsk": { ... },
    "saga": { ... },
    "mode": "...",
    "simulatedFrom": "...",
    "skillSlug": "..."
  }
}
```

### ListJobs

Request `ListJobsRequest`, response `ListJobsResponse`.

```http
POST /automaton.v1.JobsService/ListJobs HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
  "pageSize": 0,
  "pageToken": "...",
  "mode": "...",
  "skillSlug": "..."
}
```

The response:

```json
{
  "jobs": [{
    "id": "...",
    "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
    "programJson": "{}",
    "programHash": "...",
    "estimate": { ... },
    "planApprovalId": "...",
    "park": { ... },
    "stepReceipts": [{ ... }],
    "receipt": { ... },
    "lastError": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "updatedAt": "2026-01-31T09:15:00Z",
    "finishedAt": "2026-01-31T09:15:00Z",
    "pendingAsk": { ... },
    "saga": { ... },
    "mode": "...",
    "simulatedFrom": "...",
    "skillSlug": "..."
  }],
  "nextPageToken": "..."
}
```

### ListJobEvents

ListJobEvents reads one job's log in sequence order, each entry naming the
chain that caused it. It is the attribution surface over the engine, and a
job of another project reads as missing.

Request `ListJobEventsRequest`, response `ListJobEventsResponse`.

```http
POST /automaton.v1.JobsService/ListJobEvents HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "jobId": "..."
}
```

The response:

```json
{
  "events": [{
    "jobId": "...",
    "seq": 0,
    "type": "...",
    "stepId": "...",
    "at": "2026-01-31T09:15:00Z",
    "payloadJson": "{}",
    "principalChain": ["..."]
  }]
}
```

### AnswerAsk

AnswerAsk is how a person answers a question a job addressed to them.

Request `AnswerAskRequest`, response `AnswerAskResponse`.

```http
POST /automaton.v1.JobsService/AnswerAsk HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "jobId": "...",
  "requestId": "...",
  "principalId": "...",
  "continuation": "...",
  "overridesJson": "{}",
  "note": "...",
  "deliveryId": "..."
}
```

The response:

```json
{
  "job": {
    "id": "...",
    "status": "JOB_STATUS_PENDING_PLAN_APPROVAL",
    "programJson": "{}",
    "programHash": "...",
    "estimate": { ... },
    "planApprovalId": "...",
    "park": { ... },
    "stepReceipts": [{ ... }],
    "receipt": { ... },
    "lastError": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "updatedAt": "2026-01-31T09:15:00Z",
    "finishedAt": "2026-01-31T09:15:00Z",
    "pendingAsk": { ... },
    "saga": { ... },
    "mode": "...",
    "simulatedFrom": "...",
    "skillSlug": "..."
  }
}
```

### ListAsks

ListAsks reads the questions a project has addressed to people: what is
outstanding, who holds it, what was answered and by whom. Until it existed
an ask was visible only by listing jobs and reading pending_ask, so an
answered one was visible only by reading a log.

Request `ListAsksRequest`, response `ListAsksResponse`.

```http
POST /automaton.v1.JobsService/ListAsks HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "state": "ASK_STATE_PENDING",
  "principalId": "...",
  "jobId": "...",
  "standingProgramId": "...",
  "pageSize": 0,
  "pageToken": "..."
}
```

The response:

```json
{
  "asks": [{
    "pointId": "...",
    "principalId": "...",
    "hopIndex": 0,
    "deliveredPrincipals": ["..."],
    "toolSlug": "...",
    "entityId": "...",
    "toolCallId": "...",
    "deliveredAt": "2026-01-31T09:15:00Z",
    "deadlineAt": "2026-01-31T09:15:00Z",
    "approvalId": "...",
    "exhausted": true,
    "jobId": "...",
    "requestId": "...",
    "state": "ASK_STATE_PENDING",
    "question": "...",
    "answerSchemaJson": "{}",
    "continuations": ["..."],
    "escalation": [{ ... }],
    "askedAt": "2026-01-31T09:15:00Z",
    "decision": { ... },
    "stepId": "...",
    "delivery": "..."
  }],
  "nextPageToken": "..."
}
```

### GetAsk

GetAsk reads one of them.

Request `GetAskRequest`, response `GetAskResponse`.

```http
POST /automaton.v1.JobsService/GetAsk HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "jobId": "...",
  "requestId": "..."
}
```

The response:

```json
{
  "ask": {
    "pointId": "...",
    "principalId": "...",
    "hopIndex": 0,
    "deliveredPrincipals": ["..."],
    "toolSlug": "...",
    "entityId": "...",
    "toolCallId": "...",
    "deliveredAt": "2026-01-31T09:15:00Z",
    "deadlineAt": "2026-01-31T09:15:00Z",
    "approvalId": "...",
    "exhausted": true,
    "jobId": "...",
    "requestId": "...",
    "state": "ASK_STATE_PENDING",
    "question": "...",
    "answerSchemaJson": "{}",
    "continuations": ["..."],
    "escalation": [{ ... }],
    "askedAt": "2026-01-31T09:15:00Z",
    "decision": { ... },
    "stepId": "...",
    "delivery": "..."
  }
}
```

## Messages

### AnswerAskRequest

AnswerAskRequest lands one principal's answer to an outstanding ask. The
answer selects a continuation the program declared and nothing else, and the
answering key comes from the authenticated request: an approval ask resolves
its approval through policy, and policy refuses a key without the approver
role and the very key whose plan parked there.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job_id` | `string` | 1 |  |
| `request_id` | `string` | 2 | request_id is optional. Naming it refuses an answer to a question the job has already moved past. |
| `principal_id` | `string` | 3 | principal_id is the contact answering. An answer from anyone the ask has not reached is refused. |
| `continuation` | `string` | 4 | continuation is one of proceed, retry-with, skip, or abort, and it must be one the judgment point declared. |
| `overrides_json` | `string` | 5 | overrides_json carries a retry-with answer's changes, in the shape the point's answer schema declares. |
| `note` | `string` | 6 |  |
| `delivery_id` | `string` | 210 | delivery_id names the webhook delivery this answer came back on, taken from the X-Automaton-Delivery-Id header of the ask_raised delivery. It is recorded as the answer's surface and authorizes nothing: the answer is authorized by the presented key either way. Empty for an answer given directly, which records the surface as "api". Delivery is at-least-once and answering is at-most-once, so the same question can arrive twice. A second answer to an answered ask is refused, recorded on the job's log as an answer-refused entry, and moves nothing. |

### AnswerAskResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job` | `Job` | 1 |  |

### Ask

Ask is one outstanding question addressed to a named principal: who holds it
now, the channel call that carried it, and when the engine stops waiting. An
ask is a judgment point whose handler is a principal, not a separate
mechanism, so what releases the job is still an answer naming one of the
continuations the program declared.

The question text is not repeated here. It is on the pending judgment this
ask belongs to, and a job carries one of each.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `point_id` | `string` | 1 | point_id is the judgment point the program declared, or "approval" when the ask is an approval gate the engine addressed to a person. |
| `principal_id` | `string` | 2 | principal_id is the contact who holds the question now, which is the last hop the escalation ladder reached. |
| `hop_index` | `int32` | 3 |  |
| `delivered_principals` | repeated `string` | 4 | delivered_principals lists every contact the ask has reached, in hop order. Only these may answer it. |
| `tool_slug` | `string` | 5 |  |
| `entity_id` | `string` | 6 |  |
| `tool_call_id` | `string` | 7 | tool_call_id is the ledger row the delivery was recorded as. |
| `delivered_at` | `google.protobuf.Timestamp` | 8 |  |
| `deadline_at` | `google.protobuf.Timestamp` | 9 | deadline_at is when this hop's timeout fires. Unset means the ask waits without a deadline, which is what an approval ask does: no clock approves. |
| `approval_id` | `string` | 10 | approval_id is set when the ask is a policy approval gate. Answering it resolves that approval through policy, where separation of duty is enforced. |
| `exhausted` | `bool` | 11 | exhausted reports an escalation ladder that ran out with no declared default: nothing more is delivered and a person owns the question. |
| `job_id` | `string` | 200 | job_id and request_id are how an answer names this ask: AnswerAsk takes both, and request_id refuses an answer to a question the job has moved past. They and the fields below them are the read surface's (ListAsks, GetAsk); a job's pending_ask leaves them empty, because a job read carries the question on park.judgment beside it. |
| `request_id` | `string` | 201 |  |
| `state` | `AskState` | 202 |  |
| `question` | `string` | 203 | question and answer_schema_json are the judgment point's own, repeated here because a reader of an ask list has no judgment beside it to read them from. A form is generated from the schema, so a program declaring a new question shape needs no client change. |
| `answer_schema_json` | `string` | 204 |  |
| `continuations` | repeated `string` | 205 | continuations is what an answer may pick: proceed, retry-with, skip, abort, and only the ones the point declared. |
| `escalation` | repeated `AskHopSpec` | 206 | escalation is the declared ladder, whether or not it has been climbed, so a reader sees who this question reaches next and when. |
| `asked_at` | `google.protobuf.Timestamp` | 207 |  |
| `decision` | `AskDecision` | 208 | decision is set once the ask was settled, by a person or by the clock. |
| `step_id` | `string` | 209 |  |
| `delivery` | `string` | 210 | delivery is what became of this ask's publication onto the registered ask endpoints: "delivered", "pending", or "dead-letter". Empty means nothing was published to an endpoint, which is what a project that registered none reads on every ask, and the channel call in tool_call_id is a separate surface either way. It is here so an ask that defaulted on its timeout with nobody reached reads differently from one a person did not answer. |

### AskDecision

AskDecision is the recorded answer to an ask: which continuation it picked,
who picked it, and where the answer arrived from. Every surface records the
same entry, which is what keeps replay from a recorded answer a pure function.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `continuation` | `string` | 1 | continuation is one of proceed, retry-with, skip, or abort. |
| `principal_id` | `string` | 2 | principal_id is the contact addressed that answered. Empty when the clock decided. |
| `key_id` | `string` | 3 | key_id is the API key that presented the answer. A contact id is an address and authorizes nobody; the key is what was authorized, so an audit of "who decided" reads both. Empty when the clock decided, and on answers recorded before this field existed. |
| `surface` | `string` | 4 | surface is where the answer arrived from: "api" for an answer presented on AnswerAsk, "timeout" for the declared default the clock applied. |
| `note` | `string` | 5 |  |
| `hop_index` | `int32` | 6 | hop_index is the rung that answered, so "who decided" answers with the hop as well as the principal. |
| `at` | `google.protobuf.Timestamp` | 7 |  |

### AskHopSpec

AskHopSpec is one rung of a declared escalation ladder: who it reaches and how
long they hold the question. The arguments template is not on the wire, since
it is the delivery's business and not the reader's.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `principal_id` | `string` | 1 |  |
| `tool_slug` | `string` | 2 |  |
| `timeout_seconds` | `int32` | 3 |  |

### CompensationReceipt

CompensationReceipt is one act step's unwind, told honestly: how many of its
committed rows the saga tried to undo, how many it undid, and the ones it
could not, named. A stranded row is an external resource with no owner until a
human takes it, which is why the count is never rounded away.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `attempted` | `int32` | 1 |  |
| `compensated` | `int32` | 2 |  |
| `stranded` | `int32` | 3 |  |
| `stranded_row_keys` | repeated `string` | 4 |  |

### Cost

Cost is what a step or a job spent. Model spend is metered in micro-USD (one
millionth of a dollar) because one row's map call costs less than a cent, and
action_count is external effects, the unit an action budget is written in.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `model_micro_usd` | `int64` | 1 |  |
| `action_count` | `int32` | 2 |  |
| `discarded_model_micro_usd` | `int64` | 3 | discarded_model_micro_usd is the part of model_micro_usd that prices work an attempt paid for and could not report: a step killed between checkpoints spends money nothing durable records, and the resumed attempt charges the rows it re-does twice rather than leaving that spend off the receipt. It is therefore a bound, not a measurement, and model_micro_usd is an upper bound of what the project paid whenever it is above zero. |

### CostEstimate

CostEstimate is the projection a human approves.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `rows` | `int32` | 1 |  |
| `model_micro_usd` | `int64` | 2 |  |
| `action_count` | `int32` | 3 |  |
| `effects` | repeated `ProjectedEffect` | 4 |  |

### GetAskRequest

GetAskRequest reads one ask. request_id is optional: naming no request reads
the ask the job is waiting on now.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job_id` | `string` | 1 |  |
| `request_id` | `string` | 2 |  |

### GetAskResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `ask` | `Ask` | 1 |  |

### GetJobRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `id` | `string` | 1 |  |

### GetJobResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job` | `Job` | 1 |  |
| `simulation` | `Job` | 94 | simulation is the job named by job.simulated_from, receipts included, so an approver reads the measured diff beside the projected estimate in one call. Absent when the plan was not simulated first. |

### Job

Job is the folded state of one job's event log.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `id` | `string` | 1 |  |
| `status` | `JobStatus` | 2 |  |
| `program_json` | `string` | 3 | the plan verbatim, as submitted |
| `program_hash` | `string` | 4 |  |
| `estimate` | `CostEstimate` | 5 |  |
| `plan_approval_id` | `string` | 6 | set when the plan parked before step one |
| `park` | `Park` | 7 | set while status is PARKED |
| `step_receipts` | repeated `StepReceipt` | 8 |  |
| `receipt` | `Receipt` | 9 | the fold; complete is false until every step reported |
| `last_error` | `string` | 10 |  |
| `created_at` | `google.protobuf.Timestamp` | 11 |  |
| `updated_at` | `google.protobuf.Timestamp` | 12 |  |
| `finished_at` | `google.protobuf.Timestamp` | 13 |  |
| `pending_ask` | `Ask` | 60 | pending_ask is the question this job owes an answer to, when it parked on one. It mirrors park.judgment's ask so a reader learns who was asked without walking into the park. |
| `saga` | `SagaState` | 70 | saga is what became of the effects this job landed, set once an unwind has compensated anything. A job parked with stranded rows names them here. |
| `mode` | `string` | 92 | mode is what this submission was allowed to touch: "run" (the default), "simulate" (act steps projected instead of called) or "shadow" (every external call answered from recorded state). Every receipt of a measuring run carries simulated, and a simulated receipt never folds into a real one. |
| `simulated_from` | `string` | 93 | simulated_from names the simulation this plan was measured by, set when the submission asked to be simulated first. Its receipts are the diff an approver reads beside the estimate. |
| `skill_slug` | `string` | 80 | skill_slug names the promoted skill whose call submitted this job, empty when a caller submitted the program directly. Everything else about the job is the same either way. |

### JobEvent

JobEvent is one entry of a job's append-only log, which is the audit answer
to "what did this job do". Rows never cross this surface: payload_json is the
entry's own payload (a checkpoint, a receipt, a park reason), and the rows a
step read or wrote live in collections.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job_id` | `string` | 1 |  |
| `seq` | `int64` | 2 | seq is dense and 1-based per job, so the order of the page is the order the job walked and a gap means a lost write. |
| `type` | `string` | 3 | type is the log's closed vocabulary: submitted, plan-approved, step-started, checkpoint, step-completed, parked, judgment-requested, ask-delivered, judgment-recorded, answer-refused, compensation-applied, resumed, finished. |
| `step_id` | `string` | 4 |  |
| `at` | `google.protobuf.Timestamp` | 5 |  |
| `payload_json` | `string` | 6 |  |
| `principal_chain` | repeated `string` | 140 | Who caused this entry, oldest cause first: "key:<id>", "skill:<slug>", "job:<id>". It is an audit record and never an authorization input. |

### ListAsksRequest

ListAsksRequest pages one project's asks. Every filter narrows; an empty one
does not. Asks of another project are absent rather than forbidden, which is
the read rule every surface here follows.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `state` | `AskState` | 1 | state lists only asks in one state. Unspecified lists every state. |
| `principal_id` | `string` | 2 | principal_id lists only the asks this contact has been reached by, which is the inbox read: a question delivered to somebody else is not theirs to answer and is not theirs to see in this list. |
| `job_id` | `string` | 3 |  |
| `standing_program_id` | `string` | 4 | standing_program_id lists the asks raised by the jobs a standing program fired. |
| `page_size` | `int32` | 5 |  |
| `page_token` | `string` | 6 |  |

### ListAsksResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `asks` | repeated `Ask` | 1 | asks, newest question first. |
| `next_page_token` | `string` | 2 |  |

### ListJobEventsRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job_id` | `string` | 1 |  |

### ListJobEventsResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `events` | repeated `JobEvent` | 1 | The whole log in sequence order. A job's log is bounded by its own steps and checkpoints, so the page is the log rather than a window on it. |

### ListJobsRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `status` | `JobStatus` | 1 | unspecified lists every status |
| `page_size` | `int32` | 2 |  |
| `page_token` | `string` | 3 |  |
| `mode` | `string` | 95 | mode narrows the page to one submission mode. Empty lists every mode. |
| `skill_slug` | `string` | 80 | skill_slug lists only the jobs one skill submitted, which is how "what has this skill done" is answered from the log. Empty lists every job. |

### ListJobsResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `jobs` | repeated `Job` | 1 |  |
| `next_page_token` | `string` | 2 |  |

### Park

Park is why a running job stopped. approval_id names an approval in the policy
queue, judgment the question a job waits on; both are set when the approval
was delivered as an ask.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `step_id` | `string` | 1 |  |
| `reason` | `string` | 2 |  |
| `approval_id` | `string` | 3 |  |
| `judgment` | `PendingJudgment` | 4 |  |
| `at` | `google.protobuf.Timestamp` | 5 |  |

### PendingJudgment

PendingJudgment is a declared pause waiting on an answer. It carries the
question and the continuations the program declared, never rows: a decision
sees counts and reasons.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `id` | `string` | 1 |  |
| `step_id` | `string` | 2 |  |
| `handler` | `string` | 3 | model, human, or principal |
| `question` | `string` | 4 |  |
| `answer_schema_json` | `string` | 5 |  |
| `continuations` | repeated `string` | 6 | proceed, retry-with, skip, or abort |
| `breaches` | repeated `string` | 7 | the declared bounds this step exceeded, if any |
| `asked_at` | `google.protobuf.Timestamp` | 8 |  |

### ProjectedEffect

ProjectedEffect is one line of what a plan would do to the world, derived
statically from the program before anything executes. access_class is the
catalog's blast-radius class, and a destructive one is what makes a plan park
for approval.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `step_id` | `string` | 1 |  |
| `tool_slug` | `string` | 2 |  |
| `access_class` | `string` | 3 |  |
| `count` | `int32` | 4 |  |
| `compensation` | `bool` | 70 | compensation marks an effect that only happens on an unwind: the inverse the step declared. An approver reads the forward effects as what the plan intends and these as what undoing it would do. |

### Receipt

Receipt is the failure-honesty contract: how many rows went in, how many came
out, what was dropped and why, whether the producer saw everything it was
supposed to, and what it cost. A job receipt is the fold of its step
receipts, so it cannot claim more than the steps proved.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `rows_in` | `int32` | 1 | rows_in and rows_out are the plan's "in" and "out". They are spelled out here because "in" is a keyword in some generated languages. |
| `rows_out` | `int32` | 2 |  |
| `dropped` | map<`string`, `int32`> | 3 | dropped counts rows by reason. Nothing disappears without a reason. |
| `complete` | `bool` | 4 | complete is false when any producer declared a shortfall, and it is the AND over folded receipts. |
| `cost` | `Cost` | 5 |  |
| `simulated` | `bool` | 6 | simulated marks a receipt produced without touching the world. A simulated receipt is never folded into a real one. |

### SagaState

SagaState is the same account for the whole job: what its act steps landed and
still stands, what was undone, and what is stranded. It is derived from the act
steps' outcome collections and the log's compensation entries, and it is unset
for a job that never unwound anything.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `committed` | `int32` | 1 | committed is how many landed rows still stand, including the rows of act steps that declared no inverse. |
| `compensated` | `int32` | 2 |  |
| `stranded` | `int32` | 3 |  |
| `stranded_row_keys` | repeated `string` | 4 |  |

### StepReceipt

StepReceipt is one step's receipt plus what it produced.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `step_id` | `string` | 1 |  |
| `primitive` | `string` | 2 | collect, map, reduce, act, or files |
| `receipt` | `Receipt` | 3 |  |
| `output_handle` | `string` | 4 | the collection this step produced, if any |
| `reused` | `bool` | 5 | reused reports that the step did not run: an identical fingerprint already had an output collection, so the content address was served instead. |
| `attempt` | `int32` | 6 |  |
| `started_at` | `google.protobuf.Timestamp` | 7 |  |
| `finished_at` | `google.protobuf.Timestamp` | 8 |  |
| `compensation` | `CompensationReceipt` | 70 | compensation is the saga's account of this step's unwind, unset when nothing was unwound. It sits beside the receipt rather than inside it: rows in and rows out count what the step did, and undoing an effect does not touch a row twice. |

### SubmitJobRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `program_json` | `string` | 1 | program_json is the job program. JSON is the surface syntax: SDKs generate it, and any sugar compiles to it. An unknown field is a refusal, so a typo fails at submit time instead of being ignored at hour three. |
| `idempotency_key` | `string` | 2 | idempotency_key makes a repeated submission return the original job instead of starting a second run of the same plan. |
| `mode` | `string` | 90 | mode is what this submission may touch: "run" (default), "simulate" or "shadow". It rides on the request rather than in the program because the same program has to be runnable both ways: the plan a human approves and the plan that then runs are the same bytes. |
| `simulate_first` | `bool` | 91 | simulate_first submits two jobs: a simulation that runs now, and this plan, which parks for approval naming it. Two jobs, because a simulated receipt and a real one must never share a fold. |

### SubmitJobResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `job` | `Job` | 1 | job carries the status the submission reached: running, or pending plan approval with the estimate the approver decides on. |

## Enums

### JobStatus

| Value | # | Meaning |
| --- | --- | --- |
| `JOB_STATUS_UNSPECIFIED` | 0 |  |
| `JOB_STATUS_PENDING_PLAN_APPROVAL` | 1 | The plan needs a human decision before step one runs. plan_approval_id names the approval and estimate carries the numbers it was granted on. |
| `JOB_STATUS_RUNNING` | 2 |  |
| `JOB_STATUS_PARKED` | 3 | Stopped on an approval or a judgment point. park says which, so a parked job is a visible state with an owner rather than a stall. |
| `JOB_STATUS_SUCCEEDED` | 4 |  |
| `JOB_STATUS_FAILED` | 5 |  |
| `JOB_STATUS_CANCELED` | 6 |  |

### AskState

AskState is what an ask is doing now. exhausted is not a state: an ask whose
ladder ran out is still pending or escalated, because it still owes a person's
answer, and the exhausted flag says nothing more will be delivered.

| Value | # | Meaning |
| --- | --- | --- |
| `ASK_STATE_UNSPECIFIED` | 0 |  |
| `ASK_STATE_PENDING` | 1 | Outstanding, at the first hop. |
| `ASK_STATE_ESCALATED` | 2 | Outstanding, and the ladder has climbed at least one rung. |
| `ASK_STATE_ANSWERED` | 3 | A principal answered it. |
| `ASK_STATE_TIMED_OUT` | 4 | Nobody answered and the declared default applied. No approval ask reaches this state: an approval carries no default, because no clock approves. |
