Skip to main content

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.

POST/v1/reviews/:reviewId/resolve

Body

values*objectThe resolution, validating against the review's input_contract (422 on violation).
feedbackobject{ proposal_verdict: correct | incorrect | partial, correction, generalize } — the half that teaches the app. generalize: true queues a rule candidate.

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.

Feedback changes context and produces candidates; it never changes versioned logic silently. A promoted candidate lands in the draft, where the normal review-diff-publish ceremony still applies — so every behavioral change traces to a human publish, with the review as its justification.

Frequently asked questions

Who may resolve which review kinds?+
approval: humans only, and never the raiser (separation of duties). resolution and judgment: a human, or a machine caller with an operate grant on the review's app (legacy write scope for zero-grant keys). data_request: any authorized actor, including agents. The resolve endpoint enforces this per kind.
Is resolving idempotent?+
Re-submitting the same resolution is idempotent on the resolution hash; a second, different resolution of an already-resolved review is rejected with 409. Design agents to treat a 409 here as "a human (or another agent) got there first" and re-read the review.
Does the interview endpoint store the conversation?+
No. The transcript lives with the client and is sent whole on each turn; the server validates any proposed values against the input contract and returns the next turn. This keeps the endpoint stateless and lets a UI resume or abandon an interview freely.
What makes something an "exception" rather than a review?+
Provenance, not shape. Exceptions are exactly the reviews whose raised_by.type is "system" — threshold breaches, validation failures, fallback holds. GET /v1/apps/:id/exceptions is a filtered view of the same rows the reviews endpoints return.
How do precedents reach an external agent?+
Two ways: GET /v1/apps/:id/precedents (optionally similarity-filtered with ?similar_to) for explicit retrieval, and the precedents reference block assembled into each run's input package, listed in the run's precedents_applied. Withdrawn precedents stop injecting while ledger history stands.