Rulesets: JDM Format, Jurisdiction Config, and Authoring
On this page
Directory structure
rulesets/
├── federal/ # Federal parameters (updated annually by HHS/FNS/CMS)
│ ├── fpl-2026.json # Federal Poverty Level (standard, Alaska, Hawaii)
│ ├── smi-2026.json # State Median Income
│ ├── snap-allotments-2026.json # Max benefit allotment by household size (effective-dated: #1467)
│ ├── snap-deductions-2026.json # Standard deductions, shelter cap, asset limits (effective-dated)
│ ├── snap-income-limits-2026.json # 130% and 100% FPL monthly limits by household size (effective-dated)
│ ├── snap-budgeting-factors.json # Static budgeting percents + pay periods (digest-covered: #1467)
│ └── snap-alien-eligibility.json # 7 CFR 273.4 alien eligibility decision table
│
└── georgia/ # Jurisdiction-specific (one directory per state; `default/` is the reference)
├── jurisdiction.toml # All configurable thresholds and policy options
├── snap-eligibility.json # SNAP eligibility JDM ruleset
├── snap-benefit-calculation.json # SNAP benefit calculation JDM ruleset
├── tanf-eligibility.json # TANF eligibility JDM ruleset
├── medicaid-magi.json # Medicaid MAGI pathway
└── notices/ # Typst notice templates
├── manifest.toml # Maps template keys → versioned .typ files
├── components/ # Shared Orchard design system components
└── snap/ # SNAP notice/form templates
JDM format
JDM (JSON Decision Model) is the format the zen-engine rules library evaluates. Each ruleset is a JSON file with nodes and edges.
Node types
-
inputNode — entry point, receives input JSON.
-
outputNode — exit point, emits output JSON.
-
decisionTableNode — rows of input conditions → output values (most common).
-
expressionNode — key/value expressions for computed fields.
-
switchNode — conditional branching.
Hit policies
-
"first"— returns the first matching rule (most common; order matters). -
"collect"— returns all matching rules (aggregation).
Decision table structure
{
"id": "dt-example",
"type": "decisionTableNode",
"content": {
"hitPolicy": "first",
"inputs": [{"id": "in-1", "name": "Household Size", "field": "household_size"}],
"outputs": [{"id": "out-1", "name": "Limit", "field": "gross_income_limit", "type": "number"}],
"rules": [
{"in-1": "<= 3", "out-1": "1696"},
{"in-1": "4", "out-1": "3481"},
{"in-1": "", "out-1": "0"}
]
}
}
An empty string in an input cell is the default/catch-all (always matches).
zen-engine 0.55 has several non-obvious behaviors (DT string-match unreliability, no string literals in ternaries, DT outputs always stringly-typed, passThrough: true required on transform nodes). These are documented in Known Issues under "JDM Rulesets" — read it before authoring.
jurisdiction.toml
All jurisdiction-specific thresholds live here; services load at startup via CANOPY_{SERVICE}__JURISDICTION. Abbreviated shape:
[jurisdiction]
name = "State of Georgia"
fips_state_code = "13"
timezone = "America/New_York"
# #1158: OBSERVED working-day holidays per covered year — consumed by the
# shared canopy-common workday calendar (Chart B2 "5 working days",
# Chart 3730.1 month-end closure + reopen SOP). Weekend-date entries are
# refused at boot (the list is observed dates by definition); beyond
# coverage_years the arithmetic degrades to the weekend-only floor
# (household-favorable at every consuming site).
[jurisdiction.holidays]
coverage_years = [2026] # every covered year must list holidays
dates = ["2026-01-01", "2026-01-19"] # … the full observed list, cited
[snap]
bbce_enabled = true
bbce_gross_income_limit_pct_fpl = 130
bbce_asset_test_eliminated = true
initial_certification_period_months = 12
elderly_disabled_certification_period_months = 24
[snap.abawd]
qualifying_hours_per_month = 80 # Required — no silent defaults
time_limit_months = 3
window_months = 36
[snap.expedited]
low_income_limit_cents = 15000
liquid_assets_limit_cents = 10000
[snap.ipv]
first_offense_months = 12
second_offense_months = 24
third_offense_permanent = true
trafficking_permanent = true
[notices]
hearing_phone = "1-877-423-4746"
appeal_deadline_days = 90
advance_notice_days = 14
[appeals]
appeal_window_days = 90
decision_clock_days = 90
adh_notice_advance_days = 30
[tanf]
# ... full TANF/Medicaid/CAPS/WIC sections live in the file itself
Every value here must trace to an authoritative source via citations.toml (ADR-011); cargo xtask policy audit enforces completeness in CI. The federal source family is audited too (ADR-031 §1): every rulesets/federal/*.json data file must carry a file-level citation in rulesets/federal/citations.toml, and key-level citations are consistency-checked against the JSON values (--source all|jurisdiction|federal selects a family). See Configuration Reference for the full layered-config model.
Adding a ruleset
-
Create
rulesets/{jurisdiction}/{program}-{name}.json. -
Set the JSON
"name"field to"{jurisdiction}-{program}-{name}"(e.g."georgia-snap-eligibility") — theNamedFilesystemLoaderloads by this top-level name, not the filename. A mismatch produces404 rule set not found. -
canopy-rules scans
rulesets/federal/andrulesets/{jurisdiction}/at startup; rulesets are versioned in git and reloaded only on service restart (no runtime mutation path). -
Test:
POST /v1/evaluateto canopy-rules with{"rule_set_name": "georgia-snap-eligibility", …}(add?trace=truefor a node-by-node execution trace).
ADR-003 compliance
All eligibility logic must live in JDM rulesets or jurisdiction.toml, not Rust.
-
Federal regulation values (FPL, thresholds, penalty periods, qualifying hours) →
jurisdiction.tomlorrulesets/federal/*.json. -
Eligibility decision logic (income tests, categorical eligibility, alien eligibility) → JDM rulesets.
-
Rust only assembles input, calls the rules engine, and parses output.
-
Policy changes are data changes — no code deployment for threshold updates.
-
Missing required
jurisdiction.tomlkeys fail at startup with.context("missing key: …"), never silently default (thecargo xtask policy audit-unwrapsgate enforces this).