Human Reviews
Raise and resolve Human Reviews on app runs: the workspace inbox, resolver policy by kind, the interview endpoint, rule candidates, and precedent memory.
A Human Review is a structured request for human input raised by an app: run context, a question, a JSON Schema input_contract the answer must satisfy, and a feedback section. Reviews are raised by the system (threshold breaches, fallback hold), by rules whose response is *review*, by the Assisted drafting agent (with a proposal attached), by external agents, or by humans attaching judgment to a run after the fact. The exception queue (GET /v1/apps/:id/exceptions) is simply the system-raised subset. Review is a *response*, never an outcome status — resolving one is what unblocks or amends the run.
Raise with POST /v1/apps/:id/reviews (operate tier, answers 201): a kind, the question (max 4000 chars), the input_contract, optional run_id, free-form context, and optional team_id routing. List per app with GET /v1/apps/:id/reviews or across the workspace with the inbox GET /v1/reviews, both filterable by status (open, assigned, resolved, expired, cancelled), kind, and — on the inbox — app_id and assignee_user_id. Raising fires the app.review.raised webhook; resolving fires app.review.resolved.
Resolver policy depends on the kind. approval is human-only with separation of duties (raiser ≠ resolver). resolution and judgment accept a human, or a machine caller holding an operate grant on the review's app (zero-grant keys fall back to the legacy write scope). data_request accepts any authorized actor. The resolver's identity is recorded in the journal and the sealed record identically for every actor type.
/v1/reviews/:reviewId/resolveBody
curl — resolve with generalizable feedback
curl -s -X POST https://api.talonic.com/v1/reviews/$REVIEW_ID/resolve \
-H "Authorization: Bearer tlnc_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"values": { "decision": "approve", "note": "Carrier confirmed 24 pallets by phone." },
"feedback": {
"proposal_verdict": "incorrect",
"correction": "Treat a missing quantity as reviewable, not a hard hold.",
"generalize": true
}
}'Response (200) — resolved review
{
"id": "f763bd44-f428-4db0-a6a0-a441efe67c44",
"app_id": "80afca3e-c66c-4f07-abbf-78913ee80dff",
"run_id": "66c905a7-d67a-45be-a161-de11695ff564",
"kind": "judgment",
"status": "resolved",
"question": "Quantity is missing on this load. Should the load be billed anyway?",
"context": { "load": "L-2026-0847" },
"input_contract": {
"type": "object",
"required": ["decision"],
"properties": { "decision": { "enum": ["approve", "hold"] }, "note": { "type": "string" } }
},
"raised_by": { "id": "22e9f8d8-ae2b-47f9-a1fc-d29cf8440271", "type": "api_key", "label": "API client" },
"assignee_team_id": null,
"assignee_user_id": null,
"resolution": {
"by": { "type": "api_key", "id": "22e9f8d8-ae2b-47f9-a1fc-d29cf8440271", "label": "API client" },
"values": { "decision": "approve", "note": "Carrier confirmed 24 pallets by phone." },
"at": "2026-08-29T11:32:22.140Z"
},
"feedback": {
"proposal_verdict": "incorrect",
"correction": "Treat a missing quantity as reviewable, not a hard hold.",
"generalize": true
}
}The review interview
POST /v1/reviews/:reviewId/interview runs one turn of a natural-language interview that fills the resolution form for the reviewer: send message (max 4000 chars) plus the prior transcript (client-held — the server keeps no interview state), and receive reply (a focused question, or a readback of what will be submitted), proposed_values (null until intent is clear), proposed_feedback, ready, and warnings. ready is only ever true after the proposed values validate against the review's input contract. Submission stays on POST /:reviewId/resolve — the interview never resolves anything itself.
POST /v1/reviews/:reviewId/assign routes a review to a team_id (see [Review Teams](review-teams)) or a user_id; POST /v1/reviews/:reviewId/cancel withdraws it. Both are operate-tier and return the updated review / { "cancelled": true }.
Rule candidates and precedents
Feedback is never a dead end. A resolution whose feedback sets generalize: true becomes a rule candidate: GET /v1/apps/:id/rule-candidates lists the queue (each carrying the source review, the correction text, and a compiled proposed_card once promoted), POST /v1/rule-candidates/:candidateId/promote drafts the correction into the app's draft version — it never publishes — and POST /v1/rule-candidates/:candidateId/dismiss clears it. Resolved reviews also become precedents: GET /v1/apps/:id/precedents returns the approved resolved-review memory, filterable by similarity via ?similar_to=<JSON object of match keys> (422 for non-JSON), injected into Assisted drafting context and retrievable by external agents.