Wyrd docs v0.2.0 Reference GitHub

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:

text
inputs → optional preparation steps → requirements → verdict

Your 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.

Answers: Who waits for a verdict, and when do Operators run?
Your serviceSDK in a runVerifier runtimewyrd-serverBifrostScribe · OracleOperatorson_failureobserve.verify("policy-check", answer, policy)steps → requirements → decideVerificationResult · verdict, breakdowninputs + result rows · no activation✕ not called
Now

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
  1. 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.
  2. 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.
  3. The server runs the exact Verifier version inside the request. Past 60 seconds the call fails with WYRD_VERIFICATION_504_EXECUTION_TIMED_OUT.
  4. The result comes back as an ordinary value. A failed verdict is not an exception; your code reads the requirement breakdown and decides.
  5. 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.
  6. 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.
  7. The SDK uploads missing files, then posts the evidence once with one key it reuses on every transport retry.
  8. 202 confirms staging only. Replay and conflict are decided when the batched queue writer accepts the request.
  9. 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.
  10. The runner claims the queued execution and runs the same engine verify uses. Its status is separate from its verdict.
  11. The result is written to vala.verification.results. wait polls the execution until the result is visible.
  12. Only a failed verdict of background work dispatches each Operator in the binding’s on_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.

service/follows-policy.yaml yaml
# 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
VerifierShows
no-refund-promiseThe smallest Verifier: one json input and one assert check
ticket-completeA code check: inline Python that returns a verdict
follows-policyA 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.

PartHolds
inputsNamed evidence slots. Each has a type and is required unless required: false; samples slots are always required
stepsOptional preparation. Each step runs one action and produces data
requirementsEach has a unique id, a statement, and one check, and produces one verdict. Attach them to a step or to the whole Verifier
decideHow 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

typeEvidenceCaller supplies
jsonJSON, optionally constrained by schema (JSON Schema)An ordinary value
mediaA pinned file in one of formats, with an optional lower max_bytesA File (local path or bytes) or an ArtifactRef to a declared Artifact
traceA trace, read through tenant-scoped trace readsA 32-hex-digit trace ID
samplesFeature measurements or metric samples for a scheduled windowA 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 kindDoes
extractReads a media slot (from: inputs.<slot>) and returns a fixed JSON document per format. See Extraction output
agentInvokes an inline, file-authored, or registered Agent. Output follows its Prompt’s response schema, or is text
codeRuns 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 kindJudges
assertA typed comparison over one value (path) or every match of a [*] selector (each), against expected
traceThe spans in a trace slot that a JSONPath span_selector matches, compared with operator and expected (on names the slot)
agentA tool-less Agent returning the Verdict schema: outcome, reason, citations
codeSandboxed Python returning the same Verdict schema
driftPSI, 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

ReferenceMeans
inputs.<slot>…What the caller sent for that slot
steps.<id>.output…What that step returned
steps.<id>.statussucceeded, failed, or skipped
steps.<id>.requirements.<rid>.outcome / .reasonA step requirement’s verdict. Readable by actions and when, never by checks
requirement.id, requirement.statementThe 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

SituationRequirement outcome
Its step was skipped by a false when, or only by an intentionally skipped dependencynot_applicable
Its step failed or timed out, or a dependency was blocked by such a failureinconclusive
It needs output from an intentionally skipped stepnot_applicable
It needs failed output or any other missing required valueinconclusive; 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: any failed → failed; otherwise any inconclusive → inconclusive; otherwise passed.
  • pass_rate: passed divided by every applicable requirement, inconclusive ones included. At or above threshold (greater than 0, at most 1) passes. Below it, the verdict is inconclusive if any applicable result is inconclusive, otherwise failed. 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:

service/support-desk.yaml yaml
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

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.

FieldHoldsIf you leave it out
verifierA Verifier Card, by path or exact referenceRequired
runs_onA schedule or group_end TriggerThe server never runs this binding on its own
on_failureOperators, inline or as Operator CardsA 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:

yaml
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.

Evidenceverifysubmit
Run (json, media, trace; assert, trace, agent, code checks)Runs now and returns the resultQueues one execution and returns its handle
Window (samples; drift checks)Refused: use submitRecords the samples and returns Recorded

verify: run now

python
result = run.observe.verify("no-refund-promise", answer=answer)
if result.verdict != "passed":
    ...  # your service decides what to send instead

verify 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:

python
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.id

The 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.

StatusMeans
pending, running, retryingNot finished. An execution gets 3 attempts, each with a 5-minute limit
completedThe Verifier produced a verdict. The result may become visible shortly after
timed_out, errored, cancelledNo verdict exists. The execution records the error
Answers: What does this execution status mean, and what happens next?
In progressEach Operator, failed background executions onlyretryable errornext attemptresultdispatch each Operator3 attempts in 5 min○  pending▶  running↻  retrying✓  completed!  timed_out✕  errored–  cancelledverdict✓  passed●  inconclusive✕  failed○  pending▶  running✓  delivered↻  retrying✕  failed‹ an attempt ran past 5 minutes‹ not retryable, or 3 attempts used‹ stopped before a result
Now

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.

SelectClick a state to see what you would see and what to do.
Text version
  1. 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.
  2. pending. Wait. The execution is queued and no runner has claimed it.
  3. running. Wait. Each attempt has up to 5 minutes; an execution gets 3 attempts.
  4. retrying. Read error to see why the last attempt failed. It will run again.
  5. completed. Read the result from status or wait. Completed without a visible result: keep polling.
  6. timed_out. An attempt ran past 5 minutes, often a slow Agent step. No verdict exists.
  7. errored. Read error.code and error.message, such as an idempotency conflict. This is a failure to check, not a failure of your service.
  8. cancelled. The execution stopped before producing a result. Nothing to judge.
  9. verdict. Read the requirement breakdown.
  10. passed. Nothing.
  11. inconclusive. Common causes: missing required evidence, a failed step, a provider error, no applicable requirement, or too little drift data.
  12. failed. Read each requirement’s reason and citations. A direct verify pages no one.
  13. pending. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
  14. running. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
  15. delivered. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
  16. retrying. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
  17. failed. Each Operator dispatch of the failed execution. Delivery never changes the verdict.
VerdictMeansRuns Operators
passedThe evidence met the standardNo
failedThe evidence violated the standardYes, for background work of a binding
inconclusiveNo judgment was possibleNo

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 kindDoes
notifySends a Slack message or a PagerDuty event
httpCalls a URL you choose: get, post, put, patch, or delete, with optional bearer, basic, or header auth
page-support-oncall.yaml yaml
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 codeCause
WYRD_SPEC_400_INVALID_VERIFIER_REF_KINDverifier: points at a Card that is not a Verifier
WYRD_SPEC_400_INVALID_BINDING_REF_KINDruns_on: is not a Trigger, or an on_failure entry is not an Operator
WYRD_SPEC_400_DUPLICATE_VERIFICATION_BINDINGThe same Verifier version is bound twice to one subject
WYRD_SPEC_400_OPERATOR_CONNECTION_UNAVAILABLEThe 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