How verification works
A Verifier checks declared requirements against evidence and returns a verdict: passed, failed, or inconclusive. Every Verifier is built from the same four parts, whether it checks one request, reviews a PDF, inspects an agent trace, or watches a feature distribution drift:
inputs → optional preparation steps → requirements → verdictYour code runs a Verifier now with verify and acts on the result, or hands it to the server’s queue with submit. Scheduled drift runs on its own. Every path uses the same engine and writes the same result, tied to the exact Card versions that ran.
Your code sends named evidence and waits for the result, up to 60 seconds. Inputs and the result are stored afterward. Operators are never called; your code decides what to do. Press Play to step through it.
Text version
- verify. Your code sends named evidence and waits for the result, up to 60 seconds. Inputs and the result are stored afterward. Operators are never called; your code decides what to do. Press Play to step through it.
- The SDK projects each named value onto its slot. Files are hashed locally; only digests the server lacks are uploaded, straight to tenant storage, before the call.
- The server runs the exact Verifier version inside the request. Past 60 seconds the call fails with
WYRD_VERIFICATION_504_EXECUTION_TIMED_OUT. - The result comes back as an ordinary value. A
failedverdict is not an exception; your code reads the requirement breakdown and decides. - After replying, the server stages the pinned inputs and the result for
vala.verification.results. These inputs never activate queued work, and no Operator runs. - submit. Your code stages evidence and gets an execution ID back. The queue runs the same engine later; a failed verdict of a bound Verifier pages its Operators. Press Play.
- The SDK uploads missing files, then posts the evidence once with one key it reuses on every transport retry.
- 202 confirms staging only. Replay and conflict are decided when the batched queue writer accepts the request.
- Inputs are staged on the Scribe outbox. Only after Scribe acknowledges them is the queued row created. Staged work lost before that has no durable outcome.
- The runner claims the queued execution and runs the same engine
verifyuses. Its status is separate from its verdict. - The result is written to
vala.verification.results.waitpolls the execution until the result is visible. - Only a
failedverdict of background work dispatches each Operator in the binding’son_failure. Each is delivered on its own, at least once, up to 3 attempts.
The examples use the support-desk service from the quickstart and its three Verifiers.
Four parts of a Verifier
A Verifier is a Card with a normal wyrd/v1 envelope. It never names what it checks; a binding supplies the subject, so one Verifier can check any number of components.
The support desk has three, and between them they use every common building block. Switch tabs to follow each reference: follows-policy names ./policy-judge.yaml, and that Agent carries its prompt, including the refund policy, inline.
# The support agent writes a refund decision letter; this Verifier checks the
# letter against the refund policy. The policy is fixed in policy-judge's
# prompt, so the letter is the only evidence the app sends, and a new policy
# means a new judge and Verifier version. The judge calls a model, so the app
# runs this one in the background.
apiVersion: wyrd/v1
kind: Verifier
metadata:
name: follows-policy
version: 1.0.0
space: support
spec:
description: A refund decision letter follows the refund policy.
inputs:
letter: {type: media, formats: [markdown], max_bytes: 1048576}
steps:
# Turn the uploaded file into text the judge can read and cite by line.
- id: read-letter
action: {kind: extract, from: inputs.letter}
requirements:
- id: follows-policy
statement: The letter's refund decision follows the refund policy
check:
kind: agent
agent: ./policy-judge.yaml
inputs:
claim: requirement.statement
letter: steps.read-letter.output
# A tool-less judge. The refund policy lives in its prompt, so every verdict
# names the exact policy it applied. It returns a Verdict that cites the
# lines of the letter it relied on.
apiVersion: wyrd/v1
kind: Agent
metadata:
name: policy-judge
version: 1.0.0
space: support
spec:
execution_route: {kind: wyrd_gateway, deployment: openai}
run_config: {max_iterations: 1}
prompt:
provider: openai
model: gpt-6-luna
system: |
You check refund decision letters against this refund policy:
1. Refunds of up to 100 dollars are approved within 30 days of purchase.
2. Larger refunds need a manager's approval.
Cite the lines of the letter your judgment rests on.
messages:
- role: user
content: "Claim: {{claim}}. Letter: {{letter}}"
response_format:
type: object
properties:
outcome: {type: string, enum: [passed, failed, inconclusive]}
reason: {type: string}
citations:
type: array
items:
type: object
properties:
slot: {type: string}
locator:
type: object
properties:
lines:
type: object
properties:
start: {type: integer}
end: {type: integer}
required: [start, end]
additionalProperties: false
required: [lines]
additionalProperties: false
required: [slot, locator]
additionalProperties: false
required: [outcome, reason, citations]
additionalProperties: false
# The Service-wide check: whatever component answers, the desk never promises
# a guaranteed refund. One assertion, no model.
apiVersion: wyrd/v1
kind: Verifier
metadata:
name: no-refund-promise
version: 1.0.0
space: support
spec:
description: A support answer never promises a guaranteed refund.
inputs:
answer: {type: json}
requirements:
- id: no-refund-promise
statement: The answer never promises a guaranteed refund
check: {kind: assert, path: inputs.answer, operator: not_contains, expected: guaranteed refund}
# A code check: sandboxed Python reads the ticket and names every field the
# help desk needs that is missing. No model.
apiVersion: wyrd/v1
kind: Verifier
metadata:
name: ticket-complete
version: 1.0.0
space: support
spec:
description: A support ticket carries the fields the help desk needs.
inputs:
ticket: {type: json}
requirements:
- id: ticket-complete
statement: The ticket has an answer and an order id
check:
kind: code
entrypoint: check
inputs: {ticket: inputs.ticket}
source:
inline: |
def check(ticket):
missing = [field for field in ("answer", "order_id") if not ticket.get(field)]
return {
"outcome": "failed" if missing else "passed",
"reason": "missing " + ", ".join(missing) if missing else "all fields present",
"citations": [{"slot": "ticket", "locator": {"json_pointer": ""}}],
}
| Verifier | Shows |
|---|---|
no-refund-promise | The smallest Verifier: one json input and one assert check |
ticket-complete | A code check: inline Python that returns a verdict |
follows-policy | A media input, an extract step, and an agent check that judges the step’s output |
In follows-policy, the caller uploads one file: the refund decision letter the agent wrote. The read-letter step extracts its text. The check then hands policy-judge two values through its prompt placeholders: the requirement’s own statement, and the extracted letter as steps.read-letter.output. The policy itself is in the judge’s prompt, so it is fixed by the judge’s version instead of being sent with every request. The judge has no tools, and its response_format is the closed Verdict schema, so it can only answer with an outcome, a reason, and citations to lines of the letter.
| Part | Holds |
|---|---|
inputs | Named evidence slots. Each has a type and is required unless required: false; samples slots are always required |
steps | Optional preparation. Each step runs one action and produces data |
requirements | Each has a unique id, a statement, and one check, and produces one verdict. Attach them to a step or to the whole Verifier |
decide | How requirement verdicts combine: all (the default) or { pass_rate: { threshold } } |
A Verifier may have no steps. With no requirements, every result is inconclusive, never a pass. Every step and requirement ID is unique across the whole Verifier; declaration refuses duplicates.
Input slots
type | Evidence | Caller supplies |
|---|---|---|
json | JSON, optionally constrained by schema (JSON Schema) | An ordinary value |
media | A pinned file in one of formats, with an optional lower max_bytes | A File (local path or bytes) or an ArtifactRef to a declared Artifact |
trace | A trace, read through tenant-scoped trace reads | A 32-hex-digit trace ID |
samples | Feature measurements or metric samples for a scheduled window | A flat object of the declared features, or one value when the slot declares one |
Unknown slot names, wrong value types, invalid trace IDs, and values outside a declared schema or size are refused before anything runs. A missing required slot is not refused: it is missing evidence, and every check that needs it is inconclusive. An omitted optional slot is null.
Files are evidence, not Cards. Constructing a File reads nothing. During verify or submit, the SDK hashes the file, asks the server which digests it lacks, and uploads only those, straight to tenant object storage. Files are pinned by SHA-256 and never pass through the verification request. The server checks length, digest, and format when it reads them; a mismatch cannot produce a pass. An ArtifactRef selects a file of an existing declared Artifact, with that Artifact’s normal authorization.
Steps and actions
Action kind | Does |
|---|---|
extract | Reads a media slot (from: inputs.<slot>) and returns a fixed JSON document per format. See Extraction output |
agent | Invokes an inline, file-authored, or registered Agent. Output follows its Prompt’s response schema, or is text |
code | Runs immutable Python (runtime: python) from source.inline or an exact Artifact, calling entrypoint with explicitly bound inputs. Returns JSON |
Actions read and compute. They never write to external systems, declare Cards, or dispatch Operators. A step can set timeout_seconds and retries; every attempt stays visible, and retries never hide the final failure.
An Agent step has two input channels. inputs values are injected into the model request through the Prompt’s placeholders, and the keys must match those placeholders exactly. workspace maps virtual /inputs/... paths to media, JSON, or step outputs; the Agent reads them with the built-in tools named on its Agent Card. /inputs is read-only, /work is empty scratch for that call, and both are removed when the call ends. Agent tools and authorization lists the tools.
Requirements and checks
Check kind | Judges |
|---|---|
assert | A typed comparison over one value (path) or every match of a [*] selector (each), against expected |
trace | The spans in a trace slot that a JSONPath span_selector matches, compared with operator and expected (on names the slot) |
agent | A tool-less Agent returning the Verdict schema: outcome, reason, citations |
code | Sandboxed Python returning the same Verdict schema |
drift | PSI, SPC, or Custom over a samples slot (on), with full statistics |
Prefer assert whenever the check is deterministic. It is free and gives the same answer every time. Use an agent check for what an assertion cannot express, and pass the requirement text explicitly with claim: requirement.statement. A check-position Agent has no workspace and may declare only the bifrost.query tool, for example to read the spans of trace IDs it was given; registration refuses any other tool. That query reads live data under the ordinary tool and query permissions, so its results are not part of the pinned evidence.
A citation names an original input slot and a locator: page, slide, lines, json_pointer, or region. A step can return citations in its ordinary output, and a downstream judge can reuse them through its input bindings. The server checks that every cited location exists in the pinned evidence. An agent check that says passed without a valid citation becomes inconclusive.
References and order
| Reference | Means |
|---|---|
inputs.<slot>… | What the caller sent for that slot |
steps.<id>.output… | What that step returned |
steps.<id>.status | succeeded, failed, or skipped |
steps.<id>.requirements.<rid>.outcome / .reason | A step requirement’s verdict. Readable by actions and when, never by checks |
requirement.id, requirement.statement | The requirement being checked, inside its check |
Child fields use dots; array indexes are zero-based (slides[0]). Wildcards ([*]) work only in an assertion’s each. There is no $. prefix.
A step’s optional when is a condition with the same shape as an assertion, {path, operator, expected}, not an expression language; when it is false, the step is skipped. A reference in an action or when makes a dependency; depends_on adds order without data. Independent steps run concurrently. A step runs its action, then its own requirements, before anything that depends on it starts. Whole-Verifier requirements run after every step. Requirements never feed each other, and each verdict reaches decide exactly once. Declaration refuses cycles, unknown references, and references a position cannot read.
Skips, missing evidence, and decide
| Situation | Requirement outcome |
|---|---|
Its step was skipped by a false when, or only by an intentionally skipped dependency | not_applicable |
| Its step failed or timed out, or a dependency was blocked by such a failure | inconclusive |
| It needs output from an intentionally skipped step | not_applicable |
| It needs failed output or any other missing required value | inconclusive; the check is not called |
When both apply, failure wins. Provider errors, malformed output, and infrastructure failures inside one check are inconclusive with a reason, never a judgment that the subject failed.
decide ignores only not_applicable requirements. With none left, the verdict is inconclusive.
all: anyfailed→ failed; otherwise anyinconclusive→ inconclusive; otherwise passed.pass_rate: passed divided by every applicable requirement, inconclusive ones included. At or abovethreshold(greater than 0, at most 1) passes. Below it, the verdict isinconclusiveif any applicable result is inconclusive, otherwisefailed. One pass and nine inconclusive results is 10%, not 100%.
Bindings attach Verifiers to what they check
A binding lives in the declaration under verified_by. It names the Verifier, optionally the schedule that runs it, and the Operators that react to a failed background verdict. The support desk binds no-refund-promise to the whole Service and the other two to its agent component:
apiVersion: wyrd/v1
kind: Service
metadata:
name: support-desk
version: 1.0.0
space: default
spec:
service_type: agent
# The Service's own Verifier checks every answer the desk gives.
verified_by:
- verifier: ./no-refund-promise.yaml
tables:
- name: tickets
schema:
type: object
properties:
question: {type: string}
answer: {type: string}
required: [question, answer]
components:
- alias: agent
ref: ./support-agent.yaml
verified_by:
- verifier: ./ticket-complete.yaml
- verifier: ./follows-policy.yaml
- alias: prompt
ref: ./support-prompt.yaml
apiVersion: wyrd/v1
kind: Agent
metadata:
name: support-agent
version: 1.0.0
space: default
spec:
prompt: ./support-prompt.yaml
execution_route:
kind: wyrd_gateway
deployment: openai
tool_names: []
run_config:
max_iterations: 1
apiVersion: wyrd/v1
kind: Prompt
metadata:
name: support-prompt
version: 1.0.0
space: default
spec:
provider: openai
model: gpt-4o
messages:
- role: user
content: 'Answer the customer: {{question}}'
The subject is the component the binding sits under; you can also bind on the Service itself or on a standalone Agent. Changing a binding creates a new Service version, and every result records the version it checked.
| Field | Holds | If you leave it out |
|---|---|---|
verifier | A Verifier Card, by path or exact reference | Required |
runs_on | A schedule or group_end Trigger | The server never runs this binding on its own |
on_failure | Operators, inline or as Operator Cards | A failed verdict is stored, and nobody is notified |
Bind a Verifier shows a scheduled binding with its Trigger and Operator.
group_end judges a whole caller group, such as one conversation or one event, once its evidence goes quiet. The bound Verifier reads source slots such as verifiers.turn-check.inputs.turn, and callers pass the same group_id on every verify or submit of that group:
runs_on: {kind: group_end, after: 30m}after is at most one year (365d, 8760h, 525600m or 31536000s); declaration refuses a longer duration, standalone or inline, before anything is stored.
Each time Scribe acknowledges a grouped source input or result, the group becomes due after from then; new evidence moves that time later. Once it is due, one group execution judges every source input and result visible at that moment, listed in arrival order, with null for an outcome not yet visible. Later evidence makes the group due again and produces a new execution; earlier executions keep the evidence they judged. on_verdict_change Operators run when a group’s verdict differs from its previous one.
Three limits apply. Steady activity postpones the judgment for as long as it continues. A judgment uses what is visible when it reads, and evidence that becomes visible later without new activity does not schedule another. If the server dies after Scribe acknowledges evidence but before it records the due time, that activity schedules nothing; the next grouped evidence does.
When a group comes due while its binding owner is inactive or retired, or the owner’s authority is refused, the server records one errored group execution with the reason in error and drops the due group. Nothing is evaluated, no verdict or result is written and no Operator runs. The binding status’s last_execution_id names that execution, but only until a later execution replaces it; there is no list of earlier failures. Reactivating the owner does not bring the dropped group back; the group’s next evidence schedules it again. A transient server, database or query failure instead keeps the group due and retries it on a later pass.
A source execution whose result is visible but whose input is not yet visible means the read caught evidence mid-publication. An input without a result is normal and judged with a null outcome, as is an absent value inside a visible input; a missing whole input is not. The group then pins nothing and stays due, retrying on later passes. If the input is still missing more than five minutes past the group’s due time, by the database clock, the server records one errored group execution with WYRD_VERIFICATION_409_SNAPSHOT_INCOMPLETE and drops the due group in the same step. The execution IDs missing their inputs are named in the error message. Nothing is evaluated, no verdict or result is written and no Operator runs. The group’s next evidence schedules it again as usual.
Run evidence and window evidence
The check kinds decide how evidence arrives. Declaration refuses a Verifier that mixes the two, or mixes samples with other slot types.
| Evidence | verify | submit |
|---|---|---|
Run (json, media, trace; assert, trace, agent, code checks) | Runs now and returns the result | Queues one execution and returns its handle |
Window (samples; drift checks) | Refused: use submit | Records the samples and returns Recorded |
verify: run now
result = run.observe.verify("no-refund-promise", answer=answer)
if result.verdict != "passed":
... # your service decides what to send insteadverify returns within its 60-second deadline. A failed verdict is an ordinary value, not an exception. The server stores the inputs and result after it answers, but these inputs never start queued work and never run Operators. Verify a request in real time covers it end to end.
submit: queue it
submit uploads missing files, stages the evidence, and returns a verify.Execution with a server-issued execution ID:
execution = run.for_card("agent").observe.submit("follows-policy", letter=verify.File("letter.md"))
result = execution.wait(60) # here, or later from any process with execution.idThe same engine runs it later. Run a verification and inspect its evidence follows one from submission to result, including from another process.
202 means the server staged the evidence in memory, not that the queue has durably accepted it. The SDK sends one Idempotency-Key and reuses it on every transport retry. A conflicting reuse of a key is refused when the queue writer accepts the request, and you see it as an errored execution with WYRD_REGISTRY_409_IDEMPOTENCY_CONFLICT. Evidence lost before the queue accepts it, such as on an abrupt restart, has no outcome; wait ends with a timeout rather than an invented result.
Executions and verdicts
An execution’s status says whether the check ran; its verdict says what the check found.
| Status | Means |
|---|---|
pending, running, retrying | Not finished. An execution gets 3 attempts, each with a 5-minute limit |
completed | The Verifier produced a verdict. The result may become visible shortly after |
timed_out, errored, cancelled | No verdict exists. The execution records the error |
An execution first has to run, then it has a verdict, then a failed background verdict pages Operators. Click a state, or play a scenario.
Text version
- An execution first has to run, then it has a verdict, then a failed background verdict pages Operators. Click a state, or play a scenario.
- pending. Wait. The execution is queued and no runner has claimed it.
- running. Wait. Each attempt has up to 5 minutes; an execution gets 3 attempts.
- retrying. Read
errorto see why the last attempt failed. It will run again. - completed. Read the result from
statusorwait. Completed without a visible result: keep polling. - timed_out. An attempt ran past 5 minutes, often a slow Agent step. No verdict exists.
- errored. Read
error.codeanderror.message, such as an idempotency conflict. This is a failure to check, not a failure of your service. - cancelled. The execution stopped before producing a result. Nothing to judge.
- verdict. Read the requirement breakdown.
- passed. Nothing.
- inconclusive. Common causes: missing required evidence, a failed step, a provider error, no applicable requirement, or too little drift data.
- failed. Read each requirement’s reason and citations. A direct
verifypages no one. - pending. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
- running. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
- delivered. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
- retrying. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
- failed. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
| Verdict | Means | Runs Operators |
|---|---|---|
passed | The evidence met the standard | No |
failed | The evidence violated the standard | Yes, for background work of a binding |
inconclusive | No judgment was possible | No |
Each requirement in the breakdown is passed, failed, inconclusive, or not_applicable, with its reason, citations, permitted actual value, check-specific detail such as drift statistics, and the exact check, Agent, Prompt, or code that judged it. Each step reports status, attempts, duration, failure reason, and tool calls.
Results are evidence
Every result carries the execution ID, the application run_id when there is one, the exact Verifier and subject, the verdict, the requirement and step breakdown, and the digest of every input. It is written to vala.verification.results; pinned input declarations go to vala.verification.execution_inputs. Code hashes, extraction versions, Agent and Prompt versions, effective routes, and drift baselines and windows are recorded with the result.
Agent output is attributable evidence, not a promise that the same call would say the same thing again. Deterministic checks always give the same answer.
run_id is your application’s run; one run can have many executions. execution_id names one Verifier execution. Scheduled work has no run_id.
Operators react to failure
An Operator runs when background work of a binding fails. A direct verify, an inconclusive verdict, and an execution error never dispatch.
Operator kind | Does |
|---|---|
notify | Sends a Slack message or a PagerDuty event |
http | Calls a URL you choose: get, post, put, patch, or delete, with optional bearer, basic, or header auth |
apiVersion: wyrd/v1
kind: Operator
metadata:
space: default
name: page-support-oncall
version: 1.0.0
spec:
kind: notify
channel:
kind: pager_duty
connection: support-pagerduty
route: support
severity: error
summary: "{{verifier_ref}} failed for {{subject_ref}}"Secrets never go on the Card. An Operator names an operator connection, which a tenant administrator creates once. Each Operator is delivered on its own, at least once: up to 3 attempts within 5 minutes. A replayed queue request never causes a second logical dispatch, and one failing Operator does not stop the others or change the verdict.
What can go wrong at declaration
| Error code | Cause |
|---|---|
WYRD_SPEC_400_INVALID_VERIFIER_REF_KIND | verifier: points at a Card that is not a Verifier |
WYRD_SPEC_400_INVALID_BINDING_REF_KIND | runs_on: is not a Trigger, or an on_failure entry is not an Operator |
WYRD_SPEC_400_DUPLICATE_VERIFICATION_BINDING | The same Verifier version is bound twice to one subject |
WYRD_SPEC_400_OPERATOR_CONNECTION_UNAVAILABLE | The named operator connection is missing or disabled |
Registration also refuses duplicate block IDs, unknown or cyclic references, a check-position Agent with tools, and a Verifier that mixes run and window evidence, each with a structured error naming the offending field.
Verifiers judge recorded evidence, so the next page covers what a run records: Runs, observations, and retained evidence.
Last updated