# Report ONE completed sweep, at the end of every sweep, attended or unattended `POST /agent/runs` **Required scope:** `documents:write` This is how immut learns the agent ran at all: without it a sweep that protected nothing is indistinguishable from an agent that never started. Idempotent: upserts on workspace + runId, so a retried post is safe. Report only what was actually observed; never estimate a count. Uses documents:write, which every agent key already carries, so no new key is needed. ## Request body Content type: `application/json` (required) | Field | Type | Required | Description | |---|---|---|---| | `runId` | string | **yes** | your own unique id for this sweep (required; reuse it to retry the same report) | | `workspace` | string | no | workspace id | | `startedAt` | string | no | ISO 8601 | | `finishedAt` | string | no | ISO 8601 (defaults to now) | | `mode` | string | no | interactive \| unattended | | `objective` | string | no | the objective this sweep served | | `trigger` | string | no | what started it, in plain words | | `counts` | string | no | reviewed, protected, waiting, leftAlone, failed, unreadable | | `coverage` | string | no | enumerated, opened, notOpened. What you could actually read, not what you hoped to | | `connectorsReached` | string | no | array of the sources you genuinely reached this run | | `decisions` | string | no | OPTIONAL, and only with the human's explicit opt-in. Per-file decisions INCLUDING the files you decided NOT to protect: [{path, decision, docType, docState, servesObjective, confidence, folderConfidence, reason}]. Sending it means immut stores the names of documents it was never asked to protect, so ask first, default to off, and omit the field entirely on a no. An empty array is not the same as absent. decision must be one of: stored, unchanged_since_check, already_registered_elsewhere, declined_by_human, classified_pending_approval, read_not_selected, skipped_draft_wip, skipped_no_match, skipped_out_of_scope, upload_failed, undetermined_unreadable. Anything else is dropped. At most 500 are stored per run; send the ones that produced no document first, since a protected file already carries its reasoning on the document itself. | | `unreadableGroups` | string | no | OPTIONAL. Files you could not open, grouped so the customer can see the shape of what was missed without immut holding a list of their filenames: [{folder, extension, count, totalBytes, earliest, latest}]. METADATA ONLY. Never send a filename here. It is deliberately outside the decisions consent gate for that reason, and a group with no folder is dropped. At most 200 groups per run. | ## Responses | Status | Meaning | |---|---| | `200` | Success | | `400` | BadRequest | | `401` | Unauthorized | | `403` | Forbidden | | `429` | RateLimited | ## Example ```bash curl -X POST "https://backend.immut.io/api/v1/agent/runs" \ -H "Authorization: Bearer $IMMUT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"runId":"","workspace":"","startedAt":"","finishedAt":"","mode":"","objective":"","trigger":"","counts":"","coverage":"","connectorsReached":"","decisions":"","unreadableGroups":""}' ``` ## OpenAPI fragment ```json { "/agent/runs": { "post": { "operationId": "postagentruns", "summary": "Report ONE completed sweep, at the end of every sweep, attended or unattended. This is how immut learns the agent ran at all: without it a sweep that protected nothing is indistinguishable from an agent that never started. Idempotent: upserts on workspace + runId, so a retried post is safe. Report only what was actually observed; never estimate a count. Uses documents:write, which every agent key already carries, so no new key is needed.", "description": "Report ONE completed sweep, at the end of every sweep, attended or unattended. This is how immut learns the agent ran at all: without it a sweep that protected nothing is indistinguishable from an agent that never started. Idempotent: upserts on workspace + runId, so a retried post is safe. Report only what was actually observed; never estimate a count. Uses documents:write, which every agent key already carries, so no new key is needed.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "runId": { "type": "string", "description": "your own unique id for this sweep (required; reuse it to retry the same report)" }, "workspace": { "type": "string", "description": "workspace id" }, "startedAt": { "type": "string", "description": "ISO 8601" }, "finishedAt": { "type": "string", "description": "ISO 8601 (defaults to now)" }, "mode": { "type": "string", "description": "interactive | unattended" }, "objective": { "type": "string", "description": "the objective this sweep served" }, "trigger": { "type": "string", "description": "what started it, in plain words" }, "counts": { "type": "string", "description": "reviewed, protected, waiting, leftAlone, failed, unreadable" }, "coverage": { "type": "string", "description": "enumerated, opened, notOpened. What you could actually read, not what you hoped to" }, "connectorsReached": { "type": "string", "description": "array of the sources you genuinely reached this run" }, "decisions": { "type": "string", "description": "OPTIONAL, and only with the human's explicit opt-in. Per-file decisions INCLUDING the files you decided NOT to protect: [{path, decision, docType, docState, servesObjective, confidence, folderConfidence, reason}]. Sending it means immut stores the names of documents it was never asked to protect, so ask first, default to off, and omit the field entirely on a no. An empty array is not the same as absent. decision must be one of: stored, unchanged_since_check, already_registered_elsewhere, declined_by_human, classified_pending_approval, read_not_selected, skipped_draft_wip, skipped_no_match, skipped_out_of_scope, upload_failed, undetermined_unreadable. Anything else is dropped. At most 500 are stored per run; send the ones that produced no document first, since a protected file already carries its reasoning on the document itself." }, "unreadableGroups": { "type": "string", "description": "OPTIONAL. Files you could not open, grouped so the customer can see the shape of what was missed without immut holding a list of their filenames: [{folder, extension, count, totalBytes, earliest, latest}]. METADATA ONLY. Never send a filename here. It is deliberately outside the decisions consent gate for that reason, and a group with no folder is dropped. At most 200 groups per run." } }, "required": [ "runId" ] } } } }, "responses": { "200": { "description": "Success" }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" } }, "tags": [ "Agent" ], "x-required-scope": "documents:write" } } } ```