Wyrd docs v0.2.0 Reference GitHub

Status: Runnable

Monitor drift

A drift Verifier compares what a component sees in production with a baseline you declare once. Your service records samples with submit; a schedule Trigger judges each window and runs your Operators when a feature shifts. Drift is never judged in real time. This guide checks a churn model’s inputs against last quarter’s healthy data every hour.

Prerequisites

  • A declared Model Card, here churn-classifier@1.4.2 (declare one from code), and editor to declare Cards.
  • A running service that loads its bundle, as in Record a run and observations.
  • A sample of healthy inputs, with the same column names your service will record.

1. Declare the baseline

The baseline is a Data Card. From Python, declare a DataFrame of healthy inputs:

python
import pandas as pd
from wyrd.cards import Cards
from wyrd.data import DataCard, PandasInterface

healthy = pd.read_parquet("churn_inputs_2026q2.parquet")[["tenure_months", "monthly_spend"]]
baseline = DataCard(
    PandasInterface(data=healthy),
    space="default",
    name="churn-baseline",
    version="1.0.0",
)
Cards().data.declare(baseline)

Registration uploads the data and records its schema and statistics. Keep the columns to the features you will score: every feature the Verifier names must be a column here.

2. Write the Verifier

A drift Verifier has one samples slot naming the measurements it judges, and a drift check on that slot:

service/churn-input-drift.yaml yaml
apiVersion: wyrd/v1
kind: Verifier
metadata:
  space: default
  name: churn-input-drift
  version: 1.0.0
spec:
  description: Churn model inputs stay distributed like the healthy baseline.
  inputs:
    current: { type: samples, features: [tenure_months, monthly_spend] }
  requirements:
    - id: churn-inputs-stable
      statement: No churn model input drifted from the healthy baseline
      check:
        kind: drift
        on: inputs.current
        method: Psi
        signal:
          kind: Distribution
          baseline_ref: { kind: Data, name: churn-baseline, version: 1.0.0, space: default }
          features: [tenure_months, monthly_spend]
        condition: { kind: Statistical }
        profile:
          kind: Psi
          binning_strategy: { kind: Quantile, n_bins: 10 }
          threshold: { kind: Fixed, value: 0.25 }

Pick the method by what you are watching:

methodScoresprofile
PsiHow far each feature’s distribution has moved from the baseline. The usual choice for model inputsbinning_strategy (EqualWidth or Quantile, 2 to 1000 bins) and threshold: Fixed { value }, or ChiSquare { alpha } / Normal { alpha } to derive one statistically
SpcControl-chart signals on numeric features, for a value that should stay stable over time{ sample_size }, at least 2
CustomOne metric you compute against a fixed number{ metric_name, baseline_value, alert_threshold }, with signal: { kind: Metric, name } and metric_name equal to that name. Needs no baseline Data Card

condition must be Statistical. A feature is drifted when its score exceeds the threshold, and the requirement fails if any feature drifted. For PSI, 0.1 is a small shift and 0.25 a large one; start at 0.25 and tighten once you know how noisy your inputs are.

Declaration refuses a Verifier that mixes samples slots or drift checks with any other slot or check type: a window Verifier and a per-run Verifier are judged on different evidence.

3. Bind it to a schedule and declare

List the model as a component of a Service and bind the Verifier to it. The schedule reads each samples slot’s own features and judges every drift requirement in the Verifier:

service/churn-service.yaml yaml
apiVersion: wyrd/v1
kind: Service
metadata: {name: churn-service, version: 1.0.0, space: default}
spec:
  service_type: model
  components:
    - alias: churn_model
      ref: {kind: Model, name: churn-classifier, version: 1.4.2}
      verified_by:
        - verifier: ./churn-input-drift.yaml
          runs_on: {kind: schedule, cron: "0 * * * *"}

Every samples slot is required; a requirement whose slot recorded nothing in the window is inconclusive. To page someone when it fails, see Respond to a failed verification.

bash
wyrd declare service/churn-service.yaml

schedule is the only Trigger that runs a Verifier on its own. cron is evaluated in UTC unless you set tz, such as tz: America/Chicago. Each due time judges the samples recorded since the previous one, so the hourly schedule above judges [00:00, 01:00) at 01:00.

4. Wait for the baseline to fit

After declaration the server fits the baseline in the background. A PSI or SPC Verifier cannot score anything until that finishes. Check its state:

python
from wyrd.cards import CardRef, Cards

verifier = Cards().get(CardRef(kind="Verifier", name="churn-input-drift", version="1.0.0", space="default"))
print(verifier.status.verification.baseline.state)  # pending, building, ready, or failed

failed comes with baseline.error explaining why, usually a feature missing from the baseline or a text column under Spc, which only charts numbers. Fix the Card and declare a new version. A window judged before the baseline is ready is inconclusive and runs no Operator.

5. Record samples

Submit one row of feature values per prediction, on the component the binding sits under:

python
with state.run("churn_model") as run:
    features = {"tenure_months": customer.tenure_months, "monthly_spend": customer.monthly_spend}
    prediction = churn_classifier.predict(features)  # your model code
    run.observe.submit("churn-input-drift", current=features)

Values must be flat: strings, booleans, finite floats, or integers. submit writes the row through the same buffered observation path as other telemetry and returns Recorded, not an execution: there is nothing to wait on, because the schedule judges the window later. verify on this Verifier is refused with WYRD_VERIFICATION_422_INPUT_UNSUPPORTED before anything is sent.

A scheduled binding runs only while its Service is live. The server treats the binding as active when the Service’s credential has authenticated in the last 24 hours, so a schedule on a Service that is not deployed does nothing. Missed windows, such as during a server outage, are skipped, not backfilled. Each window runs as the binding owner’s authority.

6. Read the result

Each judged window writes one row to vala.verification.results, tied to the binding and the window:

python
from wyrd.bifrost import Bifrost

rows = Bifrost().sql(
    """
    SELECT window_start, window_end, verdict, verifier_version
    FROM vala.verification.results
    WHERE binding_id = $1
    ORDER BY window_start DESC
    LIMIT 20
    """,
    [binding_id],
)
print(rows.to_pandas())

binding_id is in the Service’s Card status (status.verification.binding_ids). A scheduled window has no run_id, because no application run produced it; filter on binding_id or card_uid instead. The churn-inputs-stable requirement’s detail holds every feature’s score, threshold, sample counts, and drift flag, with the window and baseline it used.

What can go wrong

Symptom or codeCauseFix
Declaration refuses the VerifierIt mixes samples or drift with other slot or check typesSplit it into a window Verifier and a per-run Verifier
Declaration refuses the bindingA window Verifier without a schedule Trigger, or a binding with inputsBind with runs_on: { kind: schedule, ... } and no inputs
WYRD_VERIFICATION_422_INPUT_UNSUPPORTEDverify on a window VerifierUse submit
Baseline stuck in failedA scored feature is missing from the baseline, or is text under SpcRead baseline.error, fix the Verifier or baseline, and declare a new version
No results on scheduleThe Service has not authenticated in 24 hours, or no samples landed in the windowDeploy the Service; submit on the bound component’s alias
Every window inconclusiveThe baseline was not ready, or the window had too few samples to scoreWait for ready; read the requirement reason

Last updated