/api/tasks/.
Typical use cases:
- LLM classifier hosted outside suchi (say, on a workstation with a bigger GPU than your NAS).
- Human review queue — an operator visits a UI, tags docs, marks each done.
- Third-party enrichment — resolve invoice numbers against your ERP, hit your VAT-validation service, etc.
- Rules that need side effects the built-in engine can’t do.
The loop
Six calls in total:kind starts with agent: are claimable from
outside. Internal chains (post-ingest, post-classify, render)
stay off-limits. Operators enqueue agent work either by
POST /api/tasks/ themselves or by wiring a producer into
postingest that creates agent:<name> jobs on the docs it wants
external attention on.
Endpoints
GET /api/tasks/?kind=agent:&state=pending
Returns pending agent-visible jobs. The kind filter is a prefix
match — agent: returns everything under that namespace,
agent:classify returns only that specific kind. state=pending
hides claims currently being worked on.
POST /api/tasks/{id}/claim
Body: {"agent_id": "my-agent-1", "ttl_seconds": 300}
Atomic lease. Succeeds only when the job is pending OR running
with an expired worker_deadline. Two agents racing on the same job
— the second gets 409 Conflict.
ttl_seconds is capped at 3600 (1 hour). Default is 300.
Response 200:
POST /api/tasks/{id}/complete
Body: {"agent_id": "my-agent-1", "last_note": "classified as invoice"}
Marks the job done. agent_id MUST match the current claim — an
agent can’t complete someone else’s work. last_note is optional and
lands in the job’s last_error column (which the tasks list surfaces
for both success and failure — think of it as “last diagnostic”).
Returns 204 No Content.
POST /api/tasks/{id}/release
Voluntary early release — call this on graceful shutdown so the job
doesn’t wait out its lease. Same agent_id check as complete.
POST /api/tasks/
Body: {"kind": "agent:my-hook", "doc_id": 17, "payload": {...}}
Operator-facing enqueue for hand-crafted work. kind must start with
agent:. payload is opaque JSON your agent will read.
Returns 201 Created with the new job id.
Auth + scopes
Every endpoint uses the standard auth middleware — a browser session cookie OR aAuthorization: Token <hex> API token works.
Scopes are enforced on the write + agent surfaces:
Browser-session principals bypass scope checks — an operator logged
into the UI can do anything the role allows. Tokens are held to the
scopes column they were issued with. Legacy tokens with the coarse
read / write labels (Phase 0/1/2) are treated as wildcards so
they keep working; new integrations should adopt the granular set.
Issue a dedicated scoped token per agent:
403 Forbidden with
{"error": "token missing required scope <name>", "code": "insufficient_scope"}.
Reference agent (Python)
A ~30-line agent that classifies everyagent:classify job it can
grab, adds a tag, and marks it done. Save as agent.py, chmod +x,
run in a loop:
Suggested job-kind conventions
There’s no built-in producer for these kinds today — you enqueue them
via
POST /api/tasks/ (from a shell, a webhook, or another agent).
A future release will wire common producers into postingest
(low-confidence LLM output automatically enqueues agent:review,
etc.).
Webhook variant (push mode)
For agents that don’t want to poll, register a webhook. On every matchingagent:* enqueue, suchi POSTs a signed envelope to the
receiver.
POST /api/agent/webhooks
Requires admin:webhooks.
Body:
kind_prefix defaults to agent: (all). Must start with agent:.
Response 201:
secret is a hex-encoded 32-byte HMAC key returned once — copy it
into the receiver’s config immediately. Future GETs never expose it.
On the server, secrets are AEAD-sealed with the same .decrypt-key
used for PDF passwords.
GET /api/agent/webhooks
Requires admin:webhooks. Lists webhooks scoped to the caller
(admins see all). Secret omitted; last_delivery_at, last_status,
last_error surface receiver health.
DELETE /api/agent/webhooks/{id}
Requires admin:webhooks. Owner-scoped for non-admins.
Signature verification
Every delivery carries two headers:X-Suchi-Event: agent.task.enqueuedX-Suchi-Signature: <hex hmac-sha256>— HMAC of the raw body, keyed with your webhook’s secret.
- Read the raw request body (do not re-JSON-encode).
- Compute
hmac_sha256(secret, body).hex(). - Constant-time compare with the header value.
- Reject any request without both headers or with a mismatched sig.
- Reject if
sent_at(in the body) is too far from now — 300s is a safe window.
POST /api/tasks/917/claim
etc.). The webhook is a notification, not a delivery of doc content.
Delivery guarantees
Every delivery goes through the durable outbox as awebhook:deliver
job. On failure (non-2xx response or network error) the outbox
retries with backoff up to the standard attempts cap; after 5
failed attempts the job lands in state='dead' on /api/tasks/ and
last_error on agent_webhooks. A webhook:deliver job whose
receiver returns a stale-secret error (AEAD open fails) deactivates
the webhook to prevent retry storms.
Not yet
- Per-agent identity separate from api_tokens — v1 uses whatever
token you present; the
agent_idin the body is bookkeeping. - Bulk claim — claim N tasks in one request. Poll-and-claim-one works for most agent loads today.