# Durable AI jobs

Keep generation separate from workspace changes, and recover correctly after disconnects and conflicts.

Use /api/ai-jobs for generation that must continue independently of a browser request. POST validates and persists a private job, then returns HTTP 202 with its public state. A worker processes the saved queue. No workspace mutation occurs merely because a job is queued, running, or completed.

```json
{
  "request": "Summarize overdue projects and cite the records.",
  "intent": "ask",
  "clientRequestId": "example_request_0001"
}
```

## Follow the saved lifecycle

| Step | Endpoint | Meaning |
| --- | --- | --- |
| Create | POST /api/ai-jobs | Returns an AiJob; use a stable clientRequestId to deduplicate creation retries. |
| List | GET /api/ai-jobs | Returns up to 20 undismissed jobs for the actor/workspace, prioritizing active requests. |
| Inspect | GET /api/ai-jobs/{id} | Reads progress, state, and any completed proposal. |
| Refine | POST /api/ai-jobs with draftJobId | Starts generation using an owned completed draft as context. |
| Edit or safely refresh | PATCH /api/ai-jobs/{id} | Changes the saved draft or safely rebases against unchanged dependencies. |
| Apply | POST /api/actions with aiJobId | Validates and commits the explicitly reviewed operations and application receipt. |
| Cancel or dismiss | DELETE /api/ai-jobs/{id} | Requests active cancellation or dismisses finished work; inspect returned status. |

Statuses are queued, running, completed, failed, and cancelled. The public AiJob includes id, workspaceId, request, input, baseRevision, status, createdAt, updatedAt, progress, optional start/finish times, and optional proposal/error/failure. Progress contains bounded stage/message/events and public text summaries. Internal workspace snapshots and model checkpoints are deliberately excluded.

## Supply bounded, valid context

A request is nonempty and at most 5,000 characters, inside a 100,000-byte object body. Optional context includes workbookId, spaceId, insightId, viewId, selectedItemIds, conversation, pendingDraft, and draftJobId. The server validates those resources against the selected workspace. intent:ask is answer-only, and viewers cannot propose editing operations. Durable jobs reject interpretationMode:built_in; use the immediate interpretation endpoint for explicit built-in help.

clientRequestId is 8–100 letters, digits, underscores, or hyphens. The current service bounds actors to two queued/running requests and the global queue to 128; limit responses are distinct from provider failure. Membership and source authorization are rechecked during work and when reading saved results. A teammate cannot read another user’s private job because they share the workspace.

## Edit the draft, not the workspace

- Send {sheetNames:[{spaceId,name}]} to rename included draft sheets. The list has 1–100 unique sheet IDs, with names up to 200 characters.
- Send {operations:[...]} to save a nonempty, domain-validated operation replacement.
- Send {rebase:true} to request a safe refresh. A changed dependency fingerprint returns draft_dependencies_changed and requires a regenerated proposal.
- Do not combine the sheetNames form with operations/rebase or add unsupported top-level keys. The PATCH body is capped at 512,000 bytes.
- Only completed, unapplied, undismissed drafts with editing permission are eligible. A successful PATCH is not an Apply action.

## Resume and apply deliberately

resumeJobId is for an undismissed failed request. Preserve its exact original request and saved context fields; changing them returns resume_context_changed. A resumed job reuses the original context/checkpoint and cumulative tool-call budget, receives another elapsed-time allowance, and dismisses its predecessor. To change the task, start a new request or refine a completed draft instead.

```json
{
  "operations": [
    {
      "action": "update_item",
      "spaceId": "sheet_projects",
      "itemId": "record_example",
      "notes": "Reviewed follow-up."
    }
  ],
  "expectedRevision": 7,
  "aiJobId": "job_example_0001"
}
```

This synthetic apply example illustrates the envelope only. Use the exact saved proposal operations and baseRevision returned by the server, and provide confirmation where required. The server verifies the saved job and commits its receipt with the mutation. An incomplete stream, provisional text, or locally invented operation is not a completed saved proposal.

> **Immediate streams are different** POST /api/interpret with stream:true produces application/x-ndjson events tied to that request. It supports status, text, activity, heartbeat, proposal, and error events. Only the final validated proposal is usable. Closing that stream is not the same interface as retrieving a durable job.

