Authoring Review Protocols
A review protocol is the recipe TfP follows to get a judgment on a result when code alone cannot settle it. It says who may review, what they are shown, what they are asked, and how their answers turn into a decision. Once written, it runs the same way every time: the same reviewer pool, the same rubric, the same rule for combining answers. A Problem can use a protocol written by someone else, and the same protocol can be reused across many Problems.
How a protocol fits into verification
Every Problem says how its results will be checked. That description is a short list of checks, run in order — for example: first an automated check, then a review. Each item on the list is one of three things: a program TfP runs, an outside system TfP asks, or a review.
A review item names a review protocol. When a Submission gets that far, TfP runs the protocol: it opens review seats that eligible reviewers claim, collects their answers, and applies the protocol's rule to reach one of three results. Accept means the Submission passed this check. Reject means it failed. Unresolved means the reviewers could not settle it, and the Submission is marked as undetermined rather than failed.
flowchart LR
VC[Verifier contract<br/>ordered checks] -->|review stage| RP[Review protocol]
RP -->|run for one Submission| RPR[Review process<br/><i>seats, claims, answers, decisions</i>]
RPR --> R[accept · reject · unresolved]Each time a protocol runs, TfP keeps a record of that run: who claimed each seat, what each reviewer answered, what assistance they declared, and how the answers were combined. The same protocols are also used on their own, without a Submission being verified, to triage or rank candidates and to audit earlier decisions.
Who does the reviewing
TfP never picks a reviewer. When a review node opens, it opens a fixed
number of seats. Anyone who is eligible under the node's rule and
not conflicted can see the open seats — over MCP with pick_review, on
the /reviews page, or through the API — and claim one. A claim
holds the seat for a short time; the reviewer reads the materials with
whatever tools they have, forms a judgment, and submits. A claim that
is not completed in time lapses and the seat reopens.
Reviewers bring their own compute. A reviewer may work by hand, or with a coding agent, another model, or any tool, on their own account; the User who claimed the seat is the accountable reviewer either way, and whatever assisted them is recorded as a declaration on the record. Your protocol can require that declaration and restrict which agents are acceptable, but TfP does not run the reviewer's agent and does not verify the declaration.
There is one exception, covered at the end of this page: a node can ask TfP itself to obtain the judgments from a hosted model. That is an advanced option, not the normal path, and no production protocol uses it.
What a protocol is made of
A protocol is a small graph with two kinds of node.
A review node asks one or more reviewers for judgment. It says how many seats to open, who is eligible to claim them, whether reviewers work independently, what they can see, and what rubric they answer. Every reviewer returns a structured response plus, when the rubric asks for them, a rationale, citations, and a confidence.
A reduce node turns answers into a decision with one deterministic
rule — count how many said pass, take the median score, pick the first
decided outcome — and emits accept, reject, or unresolved. It never
calls a model.
Nodes connect in three ways:
- a reduce node reads the answers of a review node;
- a later review node may be allowed to read earlier reviewers' answers;
- any node may be conditional on a reduce node's outcome — "only if the panel was unresolved".
One reduce node is the protocol's output. There are no loops, no expressions, no user code, no timers, and no calls out to anything.
The smallest useful protocol
One accountable reviewer decides. This is the protocol behind the live findings-extraction Problem: one seat, open to the Campaign's named reviewers; the reviewer reads the exact bound source and the submitted document, with whatever agent they choose, and answers pass, fail, or uncertain. Pass accepts, fail rejects, and uncertain leaves the result unresolved.
flowchart LR
S[Submission] --> R["reviewer<br/>1 seat · campaign reviewers"]
R --> D["decision<br/>pass → accept · fail → reject"]
D --> O[accept / reject / unresolved]The same thing written down:
version: tfp.review/v0.1
purpose: acceptance
title: "Extraction fidelity: one accountable campaign reviewer"
subject_kind: submission
nodes:
- id: reviewer
type: review
title: Campaign reviewer
reviewers:
kind: human
count: 1
independent: true
eligibility: { rule: campaign_reviewers }
visibility:
prior_nodes: []
stage_evidence: true
hidden: [submitter_identity, submitter_organization]
conflicts:
exclude_submitter: true
exclude_submitter_organization: false
skills:
- skill_version_id: skv_01jn5p2c9fxk
requirement: required
rubric:
instructions_md: >
Judge whether the submitted document is a faithful representation
of the exact bound source. You are not judging whether the paper
is right. You may use any agent or tool; you are the accountable
reviewer, and if an agent assisted you, record it as assistance.
Answer pass only if every material statement is supported by the
source; fail if any is contradicted; uncertain if the source does
not settle it. Cite the supporting passage for each finding.
response_schema:
type: object
required: [verdict, material_errors]
properties:
verdict: { enum: [pass, fail, uncertain] }
material_errors:
type: array
items:
type: object
required: [field, problem, source_locator]
properties:
field: { type: string }
problem: { type: string }
source_locator: { type: string }
rationale: required
citations: { min_items: 1 }
confidence: required
- id: decision
type: reduce
title: Reviewer decides
inputs: [reviewer]
op: k_of_n
config:
field: verdict
accept: { predicate: { op: eq, value: pass }, k: 1 }
reject: { predicate: { op: eq, value: fail }, k: 1 }
output: decision
policy:
assignment_timeout_hours: 72
claim_hold_minutes: 120What happens when it runs. The Submission's automated check passes and
the run parks on the review stage; the reviewer node opens one seat
with a 72-hour deadline. A Campaign reviewer sees the seat in
pick_review (or on /reviews), claims it, and holds it for 120
minutes. The claim returns the rubric and every material by reference:
the submitted document, the bound source files, and the required
Skill's SKILL.md, each with a hash and a Resource URI. The reviewer —
or the agent working for them — fetches and reads those, answers the
rubric, cites the passages by the same URIs, and, if an agent helped,
declares it. A pass verifies the Submission; a fail refutes it;
uncertain leaves the run undetermined. If the hold lapses before a
response, the seat reopens and any eligible reviewer, including the
same one, may claim it again. If nobody completes the seat within 72
hours, the process ends inconclusive with the reason recorded.
Declared assistance is optional here (assistance: {required: false, allowed: []} — the same as leaving it out): the rubric asks for it,
and TfP records whatever is declared. The reviewers section below
shows how to require it. This protocol, as JSON with every optional
field written out, is the canonical example
schemas/examples/review-protocol.campaign-reviewer.json, validated in
CI against the schema.
The rest of this page is about what else a review node and a reduce node can say.
Review nodes
A review node answers three questions.
Who may review. How many seats, and who is eligible to claim them —
anyone, members of one Organization, the Campaign's designated
reviewers, or named people. count is how many reviewers must
complete the node, and independent keeps them from seeing one
another's answers; both are part of what the protocol means. A node may
also require reviewers to declare the agent that assisted them and
restrict which are acceptable.
What they see. Reviewers always see the Submission and its files. You choose whether they also see earlier reviewers' reports, evidence from earlier automated checks, and any Skills you want them to apply, and what is hidden from them — the submitter's identity, for example. Conflict rules keep the submitter and, if you choose, their Organization out of the eligible pool.
What they answer. A rubric: instructions in prose, plus the exact shape of the answer as a JSON Schema — a verdict, a score, a recommendation, or a whole structured report. Separately from that answer, every reviewer can be required to give a rationale, citations into the source material, and a confidence, so those read the same way across every protocol.
Reduce nodes
A reduce node turns answers into accept, reject, or unresolved
with one of three rules:
| Rule | What it does | Use it for |
|---|---|---|
k_of_n |
Counts how many answers match a value; accepts or rejects at separate thresholds, otherwise unresolved | Votes: one reviewer decides, majority, unanimity, two-of-three-else-escalate |
threshold |
Aggregates a numeric answer (mean, median, min, max) and compares it to accept and reject cut-offs | Scored rubrics |
coalesce |
Takes the first decided outcome from a list of reducers | Joining an escalation chain into one result |
A reducer is deterministic and never calls a model. If combining answers needs judgment — three specialists' reports synthesized into one — that is another review node, and a reducer reads its answer.
Conditional branches
A node can carry when, naming a reduce node and one of its three
outcomes. The node runs only if that reducer produced that outcome;
otherwise it is skipped. That is the only conditional: escalation,
triage, and "expert only on disagreement" are all built from it, and a
coalesce at the end picks whichever branch decided.
Common shapes
| Shape | Built from |
|---|---|
| Single reviewer | one review node → k_of_n with a threshold of 1 |
| Independent panel, k of n | one independent review node → k_of_n |
| Panel with a meta-reviewer | independent panel → one reviewer who can read the panel's reports → k_of_n on the meta-reviewer |
| Sequential escalation | review → reduce → when: unresolved review → reduce → … → coalesce |
| Triage then panel | cheap review → reduce → when: accept panel → reduce → coalesce |
| Sampled audit | any of the above, run over a sample of already-accepted results, with purpose: audit |
These are available as templates that expand to ordinary protocol documents; a template has no separate behaviour.
A worked example
An escalation protocol. Three Campaign reviewers, each bringing an
agent, independently apply an extraction-accuracy rubric. A reducer
accepts if two say pass and rejects if two say fail. If neither side
reaches two — a split, or an uncertain — one domain expert from a
named list reads the Submission and the panel's reports and decides. A
final reducer takes the panel's outcome when it resolved, otherwise the
expert's.
flowchart LR
S[Submission] --> A[Reviewer A]
S --> B[Reviewer B]
S --> C[Reviewer C]
A --> PD["panel_decision<br/>k_of_n: 2 pass / 2 fail"]
B --> PD
C --> PD
PD -->|unresolved| E[Domain expert<br/>reads panel reports]
E --> ED["expert_decision<br/>k_of_n: 1"]
PD --> F["final<br/>coalesce"]
ED --> F
F --> D[accept / reject / unresolved]Five nodes: two review nodes (panel, expert) and three reduce nodes
(panel_decision, expert_decision, final). The expert branch is
conditional on panel_decision being unresolved. Written out in full:
version: tfp.review/v0.1
purpose: acceptance
title: "Extraction accuracy: three campaign reviewers, expert on disagreement"
subject_kind: submission
nodes:
- id: panel
type: review
title: Independent reviewer panel
reviewers:
kind: human
count: 3
independent: true
eligibility: { rule: campaign_reviewers }
assistance:
required: true
allowed: [] # any agent, but it must be declared
visibility:
prior_nodes: []
stage_evidence: true
hidden: [submitter_identity]
rubric:
instructions_md: >
Assess whether the extracted reaction record faithfully represents
the cited source. Answer `pass` only if every field is supported by
the source; `fail` if any field contradicts it; `uncertain` if the
source does not settle it. Cite the supporting passage for each
judgment. Declare the agent that assisted you.
response_schema:
type: object
required: [verdict]
properties:
verdict: { enum: [pass, fail, uncertain] }
rationale: required
citations: { min_items: 1 }
confidence: required
- id: panel_decision
type: reduce
title: Two of three
inputs: [panel]
op: k_of_n
config:
field: verdict
accept: { predicate: { op: eq, value: pass }, k: 2 }
reject: { predicate: { op: eq, value: fail }, k: 2 }
- id: expert
type: review
title: Domain expert
when: { node: panel_decision, outcome: unresolved }
reviewers:
kind: human
count: 1
independent: false
eligibility: { rule: named_users, user_ids: [usr_01jq7expert01] }
visibility:
prior_nodes: [panel]
stage_evidence: true
hidden: [submitter_identity]
conflicts:
exclude_submitter: true
exclude_submitter_organization: true
rubric:
instructions_md: >
The panel did not agree. Read their reports, examine the source
yourself, and decide whether the record faithfully represents it.
response_schema:
type: object
required: [verdict]
properties:
verdict: { enum: [pass, fail] }
rationale: required
citations: { min_items: 1 }
confidence: required
- id: expert_decision
type: reduce
title: Expert decides
when: { node: panel_decision, outcome: unresolved }
inputs: [expert]
op: k_of_n
config:
field: verdict
accept: { predicate: { op: eq, value: pass }, k: 1 }
reject: { predicate: { op: eq, value: fail }, k: 1 }
- id: final
type: reduce
title: Final
inputs: [panel_decision, expert_decision]
op: coalesce
config: {}
output: final
policy:
assignment_timeout_hours: 72
claim_hold_minutes: 120Node by node:
panel— three seats, open to the Campaign's reviewers, each claimant in their own independence group. Reviewers see the Submission, its Artifacts, and evidence from earlier Contract stages; the submitter's identity is hidden. Each must declare the agent that assisted them (any agent is acceptable) and returns averdictand, because the rubric requires them, a rationale, at least one citation, and a confidence. A reviewer who declines, or whose hold lapses, frees the seat for another Campaign reviewer.panel_decision— countspassandfail. Two passes accept; two fails reject; anything else, including a singleuncertain, is unresolved.expert— runs only whenpanel_decisionis unresolved. One seat, open only to the named expert, provided they are not the submitter and not from the submitter's Organization; they can read the panel's reports.expert_decision— the expert's verdict, as an accept or reject.final— the panel's outcome if it resolved, otherwise the expert's. This is the protocol'soutput.
Size count against the pool. It counts reviewers who complete,
not seats opened: if the Campaign has four named reviewers and one of
them is the submitter, a three-seat panel has exactly three people who
can fill it, and the process ends inconclusive at the deadline if any
of them does not.
Field reference
Everything in the language, in the order it appears in a document.
Element map
Every field in the language, before the detail:
| Field / block | Applies to | Purpose |
|---|---|---|
version |
protocol | Exactly tfp.review/v0.1 |
purpose |
protocol | acceptance / triage / ranking / audit — what the review means |
title |
protocol | Display name |
subject_kind |
protocol | What one process examines: submission, problem_proposal, campaign_proposal |
nodes |
protocol | The graph, 1–16 nodes |
output |
protocol | The reduce node whose outcome is the result |
policy |
protocol | The node deadline and the claim hold |
id |
node | Unique name, ^[a-z][a-z0-9_]{0,39}$ |
type |
node | review or reduce |
title |
node | Optional label |
when |
node | Run only if a named reducer emitted a named outcome |
reviewers |
review | Kind, count, independence, eligibility and declared-assistance rule (human nodes) or allowed hosted models (agent nodes) |
visibility |
review | Prior nodes visible, stage evidence, what is hidden |
conflicts |
review | Exclusions by relationship to the subject (humans; optional) |
skills |
review | SkillVersions supplied as context |
rubric |
review | Instructions, response schema, and universal-field requirements |
inputs |
reduce | Upstream nodes read |
op |
reduce | k_of_n / threshold / coalesce |
config |
reduce | The operation's parameters |
Document structure
version: tfp.review/v0.1
purpose: acceptance | triage | ranking | audit
title: string
subject_kind: submission | problem_proposal | campaign_proposal
nodes: [ ... ]
output: <id of a reduce node>
policy:
assignment_timeout_hours: 1–720
claim_hold_minutes: 15–1440 (default 120; shorter than the deadline)purpose says what the review means. A review stage of a
VerifierContract uses acceptance; triage, ranking, and audit
describe reviews run for other reasons through the same machinery and do
not change how the protocol executes. subject_kind is what one
process examines; v0 protocols review one subject at a time.
The review node
- id: panel
type: review
title: optional label
when: null | { node: <reduce id>, outcome: accept | reject | unresolved }
reviewers: { ... }
visibility: { ... }
conflicts: null | { ... } # humans; null = defaults
skills: null | [ { skill_version_id: skv_..., requirement: required | recommended } ]
rubric: { ... }Reviewers
reviewers:
kind: human # the default; see the hosted-agent section for `agent`
count: 1–25 # distinct reviewers who must complete
independent: true | false
eligibility:
rule: any_participant | organization_members | campaign_reviewers | named_users
organization_id: org_... # with organization_members; the Organization's id
# as shown in its URL, e.g. org_01jf90aa4rrc
user_ids: [usr_...] # with named_users
assistance: # optional: declared-assistance rule
required: true | false
allowed: [ { provider: string, model: string }, ... ] # empty = anycount is part of the protocol's meaning: "three independent
reviewers, two must agree" is a different protocol from "one reviewer".
The protocol fixes how many judgments are required; who supplies them
is whoever eligible claims the seats. It counts reviewers who
complete, not seats that were claimed, so size it against the pool:
a node needing five members of an Organization with six eligible
members, with the submitter excluded, has one spare.
independent: true puts each reviewer in their own independence group:
none sees another's response before completing. Use it whenever you
will count or aggregate responses.
Restricting to one Organization — only members of Organization X may claim:
reviewers:
kind: human
count: 2
independent: true
eligibility:
rule: organization_members
organization_id: org_01jf90aa4rrccampaign_reviewers opens seats to the Campaign's designated reviewers;
named_users to exactly those Users; any_participant to any
authenticated User not excluded by conflicts. No reviewer-group object
exists; eligibility is written here.
Declared assistance. A reviewer who uses an agent while forming
their judgment is still the reviewer; the assistance is recorded on the
assignment as provenance — {provider, model, config?}, with config
an optional free-form description of how the agent was run. There is
no human_with_agent kind. A node may require that declaration and
restrict it:
reviewers:
kind: human
count: 1
independent: true
eligibility: { rule: campaign_reviewers }
assistance:
required: true
allowed:
- { provider: anthropic, model: claude-opus-5 }TfP refuses a response on such a node unless it declares assistance whose provider and model match an entry in the list (any entry, when the list is empty), and records exactly what was declared. The entries are whatever strings you choose to name — TfP does not run the reviewer's agent, so it neither needs to recognise the provider nor verifies the declaration; the accountable User stands behind it.
Visibility and blinding
visibility:
prior_nodes: [panel] # earlier nodes whose responses this reviewer may read
stage_evidence: true # read-only evidence from earlier Contract stages
hidden: [submitter_identity]Reviewers always see the subject itself — the Submission and its
Artifacts. Everything else is opt-in. prior_nodes is how a
meta-reviewer reads a panel's reports; naming a node there makes this
node wait for it. hidden withholds:
hidden value |
Withheld |
|---|---|
submitter_identity |
The Attempt owner's handle and profile |
submitter_organization |
The Organization the Attempt is attributed to |
prior_scores |
The structured responses of nodes in prior_nodes; their rationale and citations stay visible |
attempt_notes |
The Attempt's Notes |
Conflicts
conflicts:
exclude_submitter: true
exclude_submitter_organization: falseApplies to human reviewers. Omit it (null) for the defaults shown; on
a hosted-agent node it has no effect and is normally omitted. Check it
against eligibility: excluding the submitter's Organization on a node
whose reviewers must be members of that same Organization leaves nobody
who can claim, and the process finishes inconclusive at the deadline.
Skills as context
skills:
- skill_version_id: skv_01jn5p2c9fxk
requirement: requiredA technoeconomic-analysis review can require the exact TEA methodology
Skill, so every reviewer applies the same method. A required Skill's
SKILL.md is listed among the reviewer's materials, by reference, and
each assignment records which SkillVersions were supplied.
Rubric
rubric:
instructions_md: >
What to assess, how to decide, what to cite.
response_schema: # JSON Schema, draft 2020-12, describing an object
type: object
required: [verdict]
properties:
verdict: { enum: [pass, fail, uncertain] }
rationale: required | optional
citations: { min_items: 0–50 }
confidence: required | optionalEvery completed assignment returns four things. The response is the
domain-specific judgment and is validated against response_schema.
The other three are universal, live on the assignment rather than
inside the response, and read the same across every protocol:
| Universal field | What it is | Rubric requirement |
|---|---|---|
rationale_md |
Communicable justification | rationale: required or optional |
citations |
Structured evidence pointers: the material's resource_uri as listed, plus locator and/or quote |
citations: { min_items: n } |
confidence |
Overall confidence in [0, 1] | confidence: required or optional |
Do not declare rationale_md, citations, or confidence inside
response_schema; validation rejects it. A response that fails the
schema, or omits a required universal field, is refused with the
problems named and nothing is recorded; the reviewer's claim stays open
so they can fix it and resubmit within the hold.
The instructions should tell reviewers what to read and what to judge, and refer to materials by the key the bundle lists them under; they should not restate a material's hash, size, or provenance — the bindings hold those facts and the bundle shows them.
Large instructions belong in a Skill
instructions_md answers one question: what judgment are you making
here? Keep it short and task-specific. Detailed methodology, a
reviewer handbook, worked examples and edge-case guidance belong in
SkillVersions pinned on the node's skills, which reviewers receive
among their materials. response_schema stays inline because it is
executable: the validator and the reducers read it. There is no
linked-instructions abstraction; a long rubric is a sign that a Skill
is missing.
Responses can be rich. A technology-analysis review might declare:
response_schema:
type: object
required: [feasibility, economics, recommendation]
properties:
feasibility: { enum: [low, medium, high] }
market: { type: string, maxLength: 4000 }
economics:
type: object
required: [capex_musd, lcoe_usd_mwh, key_assumptions]
properties:
capex_musd: { type: number }
lcoe_usd_mwh: { type: number }
key_assumptions: { type: array, items: { type: string } }
sensitivities: { type: array, items: { type: string } }
risks: { type: array, items: { type: string } }
recommendation: { enum: [pursue, watch, drop] }A reducer would read recommendation; the rest is the reviewer's
report, preserved on the assignment. Reviewers are never asked for, and
TfP never stores, hidden chain-of-thought — rationale is the
communicable justification.
The reduce node
- id: decision
type: reduce
title: optional
when: null | { node, outcome }
inputs: [panel] # one review node for k_of_n / threshold; reduce nodes for coalesce
op: k_of_n | threshold | coalesce
config: { ... }A reducer never calls a model. It reads completed responses, applies its operation, and emits an outcome plus a recorded intermediate value.
k_of_n
Counts responses whose field is a given value or in a given set, with separate accept and reject thresholds.
op: k_of_n
config:
field: verdict
accept: { predicate: { op: eq, value: pass }, k: 2 }
reject: { predicate: { op: eq, value: fail }, k: 2 }Outcome: accept if at least accept.k responses match
accept.predicate; otherwise reject if at least reject.k match
reject.predicate; otherwise unresolved. Predicates are eq (a
scalar) or in (a set) only, the accept and reject values must be
disjoint, and accept.k + reject.k must exceed the reviewer count, so
both outcomes can never hold. Numeric comparison belongs to threshold.
| Want | Config (count = n) |
|---|---|
| One reviewer decides | accept.k: 1, reject.k: 1 |
| Majority of 5 | accept.k: 3, reject.k: 3 |
| Unanimity, escalate on any split | accept.k: n, reject.k: n |
| Two of three agree, otherwise escalate | accept.k: 2, reject.k: 2 |
Accept on any pursue, reject only if all drop |
accept: {predicate: {op: in, value: [pursue]}, k: 1}, reject: {predicate: {op: eq, value: drop}, k: n} |
threshold
Aggregates a numeric field and compares it to cut-offs.
op: threshold
config:
field: score
aggregate: mean # mean | median | min | max
accept_gte: 4.0
reject_lt: 2.5Outcome: accept if the aggregate ≥ accept_gte; reject if <
reject_lt; otherwise unresolved. reject_lt must not exceed
accept_gte. Use min for "every reviewer must score at least X". The
field may be declared number or integer; mind the boundaries with
integer scores — on a 1–5 scale, reject_lt: 2 rejects only a median of
exactly 1, so write reject_lt: 3 to reject medians of 2 and below.
coalesce
Takes reduce nodes in priority order and emits the first outcome that is
not unresolved. Skipped inputs count as unresolved; if none resolved,
unresolved.
op: coalesce
inputs: [panel_decision, expert_decision]
config: {}This is how an escalation protocol produces one final result.
Why only three
The catalogue was derived from the protocols people actually asked for.
k_of_n with two thresholds already expresses single-reviewer,
majority, unanimity, and escalate-on-disagreement, so any, all,
unanimous, and map would be aliases. threshold is what scored
rubrics need. coalesce is the only way to combine an escalation chain.
Weighted votes and confidence tie-breaks were left out until a real
protocol needs them; adding an operation later invalidates no existing
protocol.
Conditional continuation
when: { node: panel_decision, outcome: unresolved }The only conditional in the language. A node with when runs if the
named reduce node emitted the named outcome, and is skipped otherwise.
Put when on both the review node and the reducer that reads it, as in
the worked example, so the whole branch is conditional. when.node
must not depend on the node carrying the condition.
Policy
policy:
assignment_timeout_hours: 72 # the node deadline, from the moment its seats open
claim_hold_minutes: 120 # how long one claim holds a seat; default 120, min 15Reviews are claimed, not assigned. When a review node opens, its seats
are visible to every eligible reviewer (pick_review), and the first to
claim a seat holds it for claim_hold_minutes; a claim that is not
completed in time lapses and the seat reopens to everyone eligible,
including the reviewer whose hold lapsed. The node as a whole must be
completed within assignment_timeout_hours of opening, or the process
finishes inconclusive with the reason recorded. The hold must be
shorter than the deadline. Keep holds short: they are what stops anyone
sitting on a seat. There is no replacement budget — a reopened seat is
just a seat — and a process never waits forever; this is the only place
that behaviour is configured. (max_replacements_per_node from earlier
documents is accepted and ignored.)
Templates as macros
The shapes listed under Common shapes are available as templates —
single, parallel_k_of_n, panel_with_meta_review,
sequential_escalation, triage_then_panel — that expand to ordinary
protocol documents. They validate exactly like hand-written protocols
and have no separate execution semantics. Sampled audit and ranking are
uses of the audit and ranking purposes, not templates.
Static validation
Every protocol is fully validated before it can be pinned or run. The complete list is in the Review reference. The rules authors hit most:
- every
inputs,prior_nodes,when.node, andoutputid must exist; - the graph must be acyclic;
outputmust be a reduce node;- a reducer's
fieldmust be a required property of its input node'sresponse_schema, with a type the operation can use; k_of_n: accept and reject values disjoint, andaccept.k + reject.kgreater than the reviewer count;threshold:reject_lt≤accept_gte;- human nodes need
eligibility; hosted-agent nodes needallowed; assistancebelongs on human nodes only;claim_hold_minutesmust be shorter thanassignment_timeout_hours;response_schemamust not declare the universal fields;prior_nodesmay not name the node itself or anything downstream of it.
Invalid examples
Each of these fails validation.
Reducer field not in the schema.
rubric:
response_schema:
type: object
required: [verdict]
properties: { verdict: { enum: [pass, fail] } }
# ...
config: { field: score, aggregate: mean, accept_gte: 4, reject_lt: 2 } # `score` does not existOverlapping accept and reject values.
config:
field: verdict
accept: { predicate: { op: in, value: [pass, uncertain] }, k: 2 }
reject: { predicate: { op: in, value: [fail, uncertain] }, k: 2 } # `uncertain` on both sidesBoth thresholds can hold.
reviewers: { kind: human, count: 3, ... }
config:
accept: { predicate: { op: eq, value: pass }, k: 1 }
reject: { predicate: { op: eq, value: fail }, k: 1 } # 1 + 1 ≤ 3Numeric comparison in k_of_n.
config:
field: score
accept: { predicate: { op: gte, value: 4 }, k: 2 } # k_of_n takes eq / in only; use thresholdUniversal field inside the response schema.
response_schema:
type: object
required: [verdict, confidence] # confidence is a universal fieldHold longer than the deadline.
policy:
assignment_timeout_hours: 1
claim_hold_minutes: 120 # 120 min ≥ 1 hCycle.
- id: a
type: review
visibility: { prior_nodes: [b], ... }
- id: b
type: review
visibility: { prior_nodes: [a], ... }Output is a review node.
output: panel # must be a reduce nodeConditional on a review node.
when: { node: panel, outcome: accept } # `when.node` must be a reduce nodeAnything the language does not have — an expression in when, a
node type other than review or reduce, an op outside the catalogue,
a URL to call — is rejected by the schema's closed enums and
additionalProperties: false.
Pinning a protocol from a Contract
# In a VerifierContract's stages
- key: faithfulness_review
kind: review
title: Source faithfulness
description_md: Reviewers judge whether the record represents the source.
review_protocol_id: rpt_01jq6p3c0gxkReviewers on nodes with stage_evidence: true can read what earlier
stages produced. Changing which protocol a stage pins changes the
Contract's acceptance semantics, so it is a new Contract and a new
Problem Revision. Campaign defaults do not reach into existing
revisions.
Advanced: TfP-hosted agent reviewer
Everything above has people claiming seats and bringing their own
agents. A review node can instead be kind: agent, which asks TfP
itself to obtain the judgments from a hosted model:
reviewers:
kind: agent
count: 3
independent: true
allowed: # exact configurations TfP may run, used round-robin
- provider: anthropic
model: claude-opus-5
config: { effort: high, max_tokens: 16000 }This is a different arrangement from the rest of the page, and the differences matter:
- TfP runs it and owns it. Nobody claims these seats. TfP issues each assignment, calls the provider on its own account, spends that execution, and is the party whose configuration produced the judgment. There is no accountable User on the assignment; the record names the provider, model, and configuration instead.
- Exact configuration matters. Because TfP is the one running the
model,
allowed[]may only name configurations TfP's hosted path knows how to execute faithfully; publication rejects anything else. Today that is one provider, and the supported configuration keys areeffort(low,medium,high,xhigh,max) andmax_tokens(256–64000). Any other key is an error, not an ignored hint. Provider defaults are resolved before the configuration is frozen on the assignment, so what the record says ran is exactly what ran. Whether the provider's credentials are configured is a runtime matter: a protocol is valid without them, and its hosted nodes execute once they are set. - Failure is handled without a person. A reply that fails the
rubric is recorded as
invalidwith the rejected response kept, and TfP issues another assignment, bounded by the node deadline. If no configured provider supports any allowed configuration, the node ends the process inconclusive. assistancedoes not apply. A hosted agent declares nothing; the configuration is the record.conflictsandeligibilityare omitted.
This path is not how participants bring agents to review — for that,
use a human node and, if you want the agent named, an assistance
rule. No production protocol needs the hosted path today; it exists for
authors who specifically want TfP to run a fixed panel. Its coverage
example is schemas/examples/review-protocol.escalate.json — a
hosted-agent panel with config: {effort: high} for its first node
and a human expert for its second, validated in CI against the schema.
It is there to exercise the hosted path, not as the default shape; the
default is the one-reviewer protocol at the top of this page.
What is recorded
Every process snapshots the exact protocol document and its hash. Every assignment records who judged (the accountable User who claimed the seat, or for a hosted node the exact agent configuration that ran), any agent assistance a reviewer declared, the Skills and prior reports supplied, what was hidden, the validated response, rationale, citations, confidence, and timestamps — including claims that were declined or lapsed, and, on a hosted node, an invalid reply with which requirement it failed. Every reducer step records its operation, config, exact inputs, and output. A decision is reconstructible from the process record alone, which is what makes review protocols comparable over time.