true or a failure message, and an optional list of fields it touches. Rules run after schema validation; if any rule fails, Boundary turns the failure into a repair message and asks the model to try again.
The Rule type
| Field | Required | Notes |
|---|---|---|
name | yes | Stable machine key. ≤128 chars, unique within the contract. Joins to per-rule failure counts on the dashboard, so don’t rename it casually. Use snake_case. |
description | no | Human-readable invariant in the positive (“Hot leads must have a score ≥ 70”), ≤1000 chars. Shown in dashboards, diffs, and code review. |
check | yes | Synchronous, deterministic, cheap. Returns true to pass, false to fail with message, or a string to fail with that string as the failure message (overrides message). |
message | no | Static fallback when check returns plain false. If check returns a string, that string wins. |
fields | no | Field names this rule reads. ≤64 entries, each ≤128 chars. Auto-inferred from the check function source when omitted — supply explicitly if you minify or your check delegates to a helper. |
A canonical rule
- The
nameissnake_caseand reads like a machine key. The dashboard joins on it. - The
descriptionis a positive statement of the invariant, not the error text. - The
checkshort-circuits the not-applicable case (lead.tier !== "hot"), then asserts the invariant (lead.score >= 70). When neither holds, it returns a dynamic failure string — context like the actual score and the threshold makes the model’s repair faster. fieldsidentifies the output fields the rule reads. Boundary can infer simple cases, but explicit fields make docs, logs, and dashboard filters easier to read.
What check may return
| Return value | Outcome |
|---|---|
true | Rule passes for this attempt. |
false | Rule fails; uses rule.message as the failure text (or “rule <name> failed” if message is also unset). |
string | Rule fails; the returned string becomes the failure text and is sent to the model verbatim. |
false with a static message — the closer the failure text is to “here’s what’s wrong, here’s what to do,” the better the repair on the next attempt.
Sync, deterministic, cheap
Synchronous
Synchronous
check is not async. Rules run inline during validation. If you need an async check (database lookup, external API), do it before or after the contract — never inside.Deterministic
Deterministic
Same input, same output. No randomness, no clocks, no external state. Determinism is what makes rules unit-testable and what lets the dashboard report stable per-rule failure counts.
Cheap
Cheap
Rules run on every attempt — up to
maxAttempts times per contract call. Keep them in-memory. No DB queries, no fetch, no heavy parsing.Domain examples
Lead scoring
Finance
Support ops
Agents
Compliance
String messages drive repair
The string a failed rule returns becomes part of the repair prompt sent to the model. Specific, contextual messages produce specific, contextual repairs.- Bad
- Good
Multiple rules
Rules are evaluated in order. All failing rules are collected — Boundary does not short-circuit on the first failure. Every violation goes back to the model in a single repair message.Validation at construction time
defineContract validates rules when you build the contract — not lazily on the first run. You catch typos, duplicate names, and over-long descriptions at startup, not in production:
nameis required, ≤128 chars, unique within the contract.descriptionis ≤1000 chars when provided.fieldsis ≤64 entries, each ≤128 chars when provided.checkmust be a function.
TypeError at defineContract time. Test it once with pnpm tsc --noEmit or your test runner; it’ll never bite you in prod.
Next steps
The Repair Loop
How violations become targeted fixes
Results
What you get back from a contract call