Status: Runnable
Author a Verifier
A Verifier is a YAML Card that says what evidence it takes and what must be true of it. This page builds the three Verifiers from the support-desk example, from cheapest to most expensive: an assertion, a Python check, then a model judge that reads a document. Start with the cheapest check that can catch the failure. Only reach for a model when nothing else can decide.
Every Verifier has the same skeleton:
apiVersion: wyrd/v1
kind: Verifier
metadata: {name: my-check, version: 1.0.0, space: support}
spec:
description: One sentence a reviewer reads in a report.
inputs: {} # the named evidence your app sends
steps: [] # optional work before checking, such as extracting a file
requirements: [] # what must be true; each has one checkEvery field is listed in the Verifier Card reference.
1. Assert on a value
The cheapest Verifier is one assertion. no-refund-promise takes the agent’s answer as JSON and fails if it contains a phrase:
# 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}
What each part does:
inputs.answernames a slot. Your app fills it when it callsverify("no-refund-promise", answer).type: jsonaccepts any JSON value; addschemato constrain it.requirementsis a list. Each requirement has anid, astatementa human reads in the result, and onecheck.kind: assertcompares the value atpathwithoperatorandexpected. No model runs, so the result is deterministic and returns in milliseconds.
path can reach inside a value, such as inputs.ticket.order_id. To check every item of a list, use each: inputs.items[*].price in place of path; the requirement passes only if every item does.
2. Check with Python
When one operator cannot express the rule, write it in Python. ticket-complete names every field the help desk needs that is missing:
# 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": ""}}],
}
The function named by entrypoint receives the inputs you map to it and returns a verdict:
| Field | Value |
|---|---|
outcome | passed, failed, inconclusive, or not_applicable |
reason | One sentence that ends up in the result |
citations | Where in the evidence the decision rests, as {slot, locator} |
The code runs in Wyrd’s sandbox, not in your app. Keep it to the decision; anything with side effects belongs in your app.
A passed verdict must cite the evidence it read. Here {"json_pointer": ""} cites the whole ticket. A passed verdict with no valid citation is recorded as inconclusive, so a check cannot pass without showing its work.
3. Judge a document with a model
Some rules need judgment. The support agent writes a refund decision letter, and follows-policy asks a model whether the letter follows the refund policy. This takes three Cards: the Verifier, the judge Agent, and the Service that binds the Verifier to the support agent.
# 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
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
Read it top to bottom:
inputs.letteris amediaslot. It accepts only Markdown files up to 1 MiB, so a PDF or a 50 MB upload is refused before any model runs.- The
read-letterstep extracts the file into{format, text}. A{{letter}}placeholder takes text, not a file, so the letter goes throughextractfirst. Steps run before requirements, and a requirement reads a step’s result assteps.read-letter.output. - The
agentcheck sends the requirement’s statement and the extracted letter topolicy-judge. Those names fill{{claim}}and{{letter}}in the judge’s prompt. policy-judgehas the policy in its system message and a closedresponse_format, so the model can only answer with an outcome, a reason, and citations at lines of the letter.
The policy lives in the judge, not in the request. Your app sends only the letter, and every verdict is tied to the exact judge version, and so the exact policy, that produced it. A new policy is a new policy-judge version.
A model call takes seconds, so the app submits this Verifier in the background instead of waiting on it in the request.
4. Check it before you declare
wyrd declare --check loads every Card a Service references, follows the ./ paths, and validates each one without writing anything:
wyrd declare --check service/support-desk.yamlplan valid: 7 card(s)The CLI then lists each Card it would declare. A broken Verifier fails here with the field that is wrong:
| You see | Cause | Fix |
|---|---|---|
WYRD_REGISTRY_400_INVALID_CARD_SPEC naming an unknown field | A misspelled key; Verifier specs reject fields they do not define | Fix the spelling against the reference |
WYRD_SPEC_400_VALIDATION with rule unknown_reference | A reference to an input, step, or requirement that does not exist, such as steps.read-leter.output | Match the slot or step id |
WYRD_SPEC_400_VALIDATION with rule duplicate_id | A step and a requirement share an id | IDs are unique across the whole Verifier |
WYRD_SPEC_400_VALIDATION with rule media_as_text | A media slot is passed straight to an Agent text placeholder | Add an extract step and pass steps.<id>.output, as in step 3 |
WYRD_SPEC_400_VALIDATION with rule extract_format | An extract step reads a slot that accepts a format extraction cannot read, such as png | Accept only PDF, text, Markdown, CSV, JSON, HTML, DOCX, XLSX, or PPTX in that slot |
WYRD_LOADER_400_INVALID_ENVELOPE | apiVersion, kind, or metadata is missing or misspelled | Copy the skeleton above |
Then bind the Verifier to a component and call it from your app.
Last updated