Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.retab.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

When a workflow reaches a gated block — a block with config.hil whose predicate flagged its output for review — the run pauses and a review overlay is created. The overlay is a versioned sidecar attached to one (run_id, block_id) pair. It is the single object you read and mutate to walk a review from “the model produced this” to “a human approved (or corrected, or rejected) it, and the run resumed.” Everything is driven through client.workflows.runs.reviews.*.
The overlay replaces the legacy v1 HIL decision endpoints (/v1/workflows/runs/{run_id}/hil-decisions and the agent-review endpoint). Those have been removed — the review overlay is the only HIL surface.

The overlay shape

A ReviewOverlay carries four logs plus a compare-and-swap token:
FieldWhat it holds
versions[]Every version of the gated block’s output. Sequence 0 is always the model’s original. Each later sequence is a corrective edit.
decisions[]Every verdict submitted against the overlay.
audit[]The append-only audit trail — every claim, release, edit, and decision.
claimThe ReviewClaim currently held on the overlay ({holder, claimed_at, expires_at}), or null.
revThe overlay’s compare-and-swap token. See the CAS contract.
Every actor — whether a model, a managed agent, or a human — is described by the same symmetric Actor shape:
# Actor — one shape, three kinds
{
    "kind": "human",          # "model" | "agent" | "human"
    "id": "user_42",
    "display_name": "Dana Rivera",
}
This means versions[].author, decisions[].decided_by, audit[].actor, and claim.holder are all the same type — you never special-case “was this a person or a model.”

The version_stamp contract

The overlay is mutated concurrently — a human in the dashboard, an agent reviewer, and your own integration can all touch the same overlay. To keep those writers from clobbering each other, every mutating call is a compare-and-swap.
  1. Read the overlay with reviews.get(...). It carries a rev — the overlay’s compare-and-swap token.
  2. Pass that rev back (as the version_stamp request parameter) on the next mutating call — edit, approve, reject, claim, release.
  3. If the overlay advanced since you read it, the stamp is stale and the call fails with HTTP 409. Re-read the overlay and retry with the fresh stamp.
In Python a stale stamp raises retab.exceptions.ConflictError; in JavaScript it surfaces as an APIError with .status === 409.
from retab import Retab
from retab.exceptions import ConflictError

client = Retab()

def with_retry(run_id, block_id, mutate):
    """Re-read and retry once on a stale version_stamp."""
    for attempt in range(2):
        overlay = client.workflows.runs.reviews.get(run_id, block_id)
        try:
            return mutate(overlay)
        except ConflictError:
            if attempt == 1:
                raise  # someone else is racing us hard — give up
    # unreachable

with_retry(
    "run_abc123", "block_extract",
    lambda o: client.workflows.runs.reviews.approve(
        "run_abc123", "block_extract", version_stamp=o.version_stamp,
    ),
)
A 409 is expected, not exceptional — it just means another reviewer or the agent got there first. Always re-read the overlay before retrying; never reuse the old stamp or fabricate one.

The review loop

1. Find the work

List the queue to find overlays awaiting a decision. Filter by workflow_id, status, or mine (overlays you hold the claim on).
queue = client.workflows.runs.reviews.list(status="awaiting_review", limit=50)

for item in queue.data:
    print(item.workflow_run_id, item.block_id, item.claim)

2. Claim it (optional, advisory)

Claiming records you in claim so a shared inbox shows the item is being worked. It does not lock the overlay — the version_stamp CAS is the real concurrency guarantee. Submitting a decision does not require holding the claim.
overlay = client.workflows.runs.reviews.get("run_abc123", "block_extract")
overlay = client.workflows.runs.reviews.claim(
    "run_abc123", "block_extract", version_stamp=overlay.version_stamp,
)
If you step away without deciding, release it so someone else can pick it up:
client.workflows.runs.reviews.release(
    "run_abc123", "block_extract", version_stamp=overlay.version_stamp,
)

3. Inspect the output

The effective output is the latest version. The model’s original is always sequence 0.
overlay = client.workflows.runs.reviews.get("run_abc123", "block_extract")

original = overlay.versions[0]          # what the model produced
effective = overlay.versions[-1]        # what an approve would flow downstream
print(effective.snapshot)

4. Decide

There are two verdicts:
VerdictEffect on the run
approvedThe effective output flows downstream and the run resumes.
rejectedThe run is cancelled. A reason is required.
overlay = client.workflows.runs.reviews.get("run_abc123", "block_extract")
stamp = overlay.version_stamp

# Approve — the latest version flows downstream, the run resumes
client.workflows.runs.reviews.approve("run_abc123", "block_extract", version_stamp=stamp)

# Reject — the run is cancelled
client.workflows.runs.reviews.reject(
    "run_abc123", "block_extract",
    version_stamp=stamp,
    reason="Source document is illegible; cannot verify totals.",
)

Approve with edits

When the model’s output is close but wrong, you don’t reject — you correct and approve. Pass edited_output to approve and the server appends a corrective version and then approves it in one atomic step. The model’s original is preserved as version 0; your corrected payload becomes the new effective version.
overlay = client.workflows.runs.reviews.get("run_abc123", "block_extract")

client.workflows.runs.reviews.approve(
    "run_abc123", "block_extract",
    version_stamp=overlay.version_stamp,
    edited_output={"total_amount": 325, "vendor_name": "Acme Corp"},
)
If you want to record a correction without deciding yet — for example, to hand a half-corrected overlay to a senior reviewer — append the version on its own with edit:
overlay = client.workflows.runs.reviews.edit(
    "run_abc123", "block_extract",
    snapshot={"total_amount": 325, "vendor_name": "Acme Corp"},
    version_stamp=overlay.version_stamp,
    origin="human_edit",
    note="Corrected total; flagging vendor name for a second look.",
)
Both paths preserve the full history — every version, with its author, is queryable forever in overlay.versions.

command_id idempotency

Every mutating method accepts an optional command_id. A retried call carrying the same command_id is deduplicated server-side — safe to use when a network blip leaves you unsure whether your decision landed.
client.workflows.runs.reviews.approve(
    "run_abc123", "block_extract",
    version_stamp=overlay.version_stamp,
    command_id="approve-run_abc123-block_extract-attempt-1",
)

Waiting for a decision

If your integration submits a gated run and needs to block until a human (or agent) resolves it, use wait_for. It polls the overlay until it leaves the awaiting_review state and returns the settled overlay.
run = client.workflows.runs.create(workflow_id="wf_abc123", ...)

# Block until the gated block at block_extract is approved or rejected
overlay = client.workflows.runs.reviews.wait_for(
    run.id, "block_extract",
    timeout=120.0,        # seconds — raises on expiry
    poll_interval=2.0,
)

if overlay.status == "approved":
    final = overlay.versions[-1].snapshot
    print("approved output:", final)
else:
    print("review ended as:", overlay.status)

API reference

EndpointMethod
List Reviewsreviews.list(...)
Get Reviewreviews.get(run_id, block_id)
Append Versionreviews.edit(...)
Submit Decisionreviews.approve / reject(...)
Claim Reviewreviews.claim(...)
Release Reviewreviews.release(...)