Routing rules
Write your review-routing intent in plain English. PURA's Translator compiles it once into deterministic CEL, and the Engine evaluates it per PR — no LLM in the hot path.
Writing pura.md
.pura/pura.md is the human source of truth — bullet points of intent. The Translator compiles it into .pura/rules.yaml (committed by the PURA app identity). You author intent; PURA produces deterministic rules.
# pura rules
- Dependabot PRs: cheapest available model.
- payments/ or billing/ with > 200 lines: Opus, high thinking.
- @mercato/search code:
- Until 50% org budget consumed: GPT-5 high.
- After: GPT-5 low.
- "security" label: Opus.
- Below $100 org budget remaining: GPT-5-mini for non-security PRs.
- Drafts: skip.
- Default: Sonnet, medium.Two homes for config. pura.md lives in the repo and holds routing rules only. Budgets, caps, allowed-model lists, BYO keys, and ZDR live in the dashboard — and repo rules can never escape org caps. Never hand-edit rules.yaml.
The CEL evaluation context
Compiled rules use when: CEL expressions over a fixed schema. The pr.* fields are deterministic at a given head SHA; the budget.* fields are time-varying.
pr.title string
pr.body string
pr.labels list<string>
pr.author string
pr.author_type string # "User" | "Bot"
pr.is_bot_author bool
pr.is_draft bool
pr.base_ref string
pr.head_ref string
pr.changed_paths list<string>
pr.lines_added / lines_deleted int
pr.lines_changed / files_changed int
pr.codeowners list<string>
pr.repo string
pr.repo_visibility string # "public" | "private" | "internal"
budget.org_cap_remaining_usd double # null if no org cap
budget.org_cap_remaining_pct double # null if no org cap
budget.org_spend_mtd_usd double
budget.project_cap_remaining_usd double # null if no project cap
budget.project_cap_remaining_pct double # null if no project cap
budget.project_spend_mtd_usd double
budget.org_hard_cap_breached bool
budget.project_hard_cap_breached boolNull when unset. The numeric *_usd / *_pct fields are null when no cap is configured — always guard with != null before comparing. The *_hard_cap_breached booleans are always set (false when no cap), and *_remaining_usd is clamped at zero.
The compiled rules.yaml
The pura.md above compiles into deterministic rules. A rule has either action: skip or use, never both. The default block is required and cannot be a skip.
version: 1
default:
provider: anthropic
model: claude-sonnet
thinking: medium
rules:
- name: skip-drafts
when: 'pr.is_draft'
action: skip
- name: dependabot
when: 'pr.author == "dependabot[bot]"'
use: { provider: openai, model: gpt-5-mini, thinking: none }
- name: payments-critical
when: 'pr.changed_paths.exists(p, p.startsWith("payments/") || p.startsWith("billing/")) && pr.lines_changed > 200'
use: { provider: anthropic, model: claude-opus, thinking: high }
- name: search-team-high-budget
when: 'pr.codeowners.exists(o, o == "@mercato/search") && budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.5'
use: { provider: openai, model: gpt-5, thinking: high }
- name: search-team-low-budget
when: 'pr.codeowners.exists(o, o == "@mercato/search") && (budget.org_cap_remaining_pct == null || budget.org_cap_remaining_pct <= 0.5)'
use: { provider: openai, model: gpt-5, thinking: low }
- name: security-label
when: '"security" in pr.labels'
use: { provider: anthropic, model: claude-opus, thinking: high }
- name: low-budget-cheap
when: 'budget.org_cap_remaining_usd != null && budget.org_cap_remaining_usd < 100 && !("security" in pr.labels)'
use: { provider: openai, model: gpt-5-mini, thinking: none }anthropic and openaiare the catalog's current providers; the schema accepts any catalog provider. Valid model ids also come from the catalog — they are not hardcoded in the schema.
Conflict resolution — bigger model wins
When multiple rules match the same PR, the most powerful model winsper the catalog's power ordering. Ties break by thinking (high > medium > low > none), then by file order. When a skip rule and a use rule both match, the use rule wins.
Budget rules don't auto-override high-stakes rules. In the example above, low-budget-cheap does not override payments-critical — Opus wins the tie. To make budget pressure win, fold the budget condition into the high-stakes rule's own when:.
To step thinking down by condition on the same model, write mutually-exclusive when: expressions so only one rule matches — as search-team-high-budget and search-team-low-budget do. They split on the budget threshold, so conflict resolution never engages between them.
A budget-tiered model ladder
A common pattern: spend the best model early in the month, then step down as the org budget burns. Spend is 1 − remaining, so "50% spent" is remaining_pct == 0.5. Express the ladder as mutually-exclusive bands on budget.org_cap_remaining_pct that leave no gap:
| Spent | Remaining % | Model |
|---|---|---|
| 0–50% | > 0.5 | gpt-5 · high |
| 50–75% | 0.25 – 0.5 | gpt-5-mini · medium |
| 75–100% | ≤ 0.25 | gpt-5-nano · none |
rules:
- name: budget-0-50-spent
when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.5'
use: { provider: openai, model: gpt-5, thinking: high }
- name: budget-50-75-spent
when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.25 && budget.org_cap_remaining_pct <= 0.5'
use: { provider: openai, model: gpt-5-mini, thinking: medium }
- name: budget-75-100-spent
when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct <= 0.25'
use: { provider: openai, model: gpt-5-nano, thinking: none }No cap, no ladder. The three bands are mutually exclusive and cover all of 0–100% remaining. When no org cap is set, org_cap_remaining_pct is null, none match, and routing falls to default. Swap in project_cap_remaining_pct to tier on the per-project cap instead.
CODEOWNERS & path matching
PURA hydrates pr.codeownersfrom the repo's CODEOWNERS file and pr.changed_paths from the diff — ideal for routing per-team in a monorepo without forking:
# Route a team's code to GPT-5 high
when: 'pr.codeowners.exists(o, o == "@mercato/search")'
# Path prefix match (any changed file under a directory)
when: 'pr.changed_paths.exists(p, p.startsWith("payments/"))'
# Combine path + size for high-stakes diffs
when: 'pr.changed_paths.exists(p, p.startsWith("billing/")) && pr.lines_changed > 200'
# Cheap model for bot authors
when: 'pr.is_bot_author'Because routing is deterministic, the same PR always routes the same way at a given head SHA (budget permitting) — different paths route to different models without any branching in your code.