PolicyEngine model development
Engineering patterns for the country-model repos — policyengine-us, policyengine-uk,
policyengine-canada. These packages encode enacted law (and proposed reforms) as variables
(Python formulas) driven by parameters (YAML), validated by YAML tests, all running on
the vectorized policyengine-core engine.
This skill is the how-to-write-it layer. To run calculations or score reforms, use the
policyengine skill instead. Verified against policyengine-us 1.764.x / policyengine-core 3.30.x
(2026-07).
For delegated implementation, test authoring or review, follow
agent-loading.md: every worker loads this entrypoint in
its own context, reads its task references and reports evidence of the load before work.
Mental model
- Parameters are a YAML tree under
parameters/gov/.... Each leaf is a dated value (or a
bracket/breakdown table) with required metadata. Accessed in formulas as
parameters(period).gov.agency.program.thing. Federal under gov/{agency}/, state under
gov/states/{st}/, contributed reforms under gov/contrib/.
- Variables live one-per-file under
variables/gov/..., each a Variable subclass with
value_type / entity / definition_period / metadata and either a formula or an
adds/subtracts list (never both). Formulas run vectorized over the whole population.
- Entities nest:
Person → TaxUnit / SPMUnit / Family / MaritalUnit / Household
(US); Person → BenUnit / Household (UK). Aggregation across entity levels is automatic
via adds/add().
- Tests are YAML files mirroring the variable path under
tests/policy/baseline/...,
asserting outputs for a described household at a period.
The absolute musts
- Never hardcode a numeric policy value in a formula. Every threshold, rate, and amount
comes from a parameter (
p.income_limit, not 2000). Bare 0/1/-1 and MONTHS_IN_YEAR
are the only acceptable literals. See references/variables.md.
- Vectorize everything. No
if/elif/else or and/or/not on entity arrays — use
where / select / & / | / ~. Python if is allowed only on scalar parameters
(if p.flat_applies:). See references/vectorization.md.
adds/subtracts XOR formula — never both in one variable. A pure sum uses the adds
attribute with no formula; anything else uses a formula (with add() inside). See
references/periods-and-aggregation.md.
- Get the period right. From a MONTH formula, YEAR flow variables (income) use
period
(auto ÷12); YEAR stocks/counts/ages/booleans use period.this_year (no division). See
references/periods-and-aggregation.md.
uv run for everything. uv run pytest ..., uv run policyengine-core test .... Never
bare pytest. Format with uv run ruff (line length 88, the default — not 79). There is
no black in this toolchain.
- Changelog = a towncrier fragment, never an edit to
CHANGELOG.md. Write
changelog.d/{branch-name}.{added|fixed|changed}.md with one line.
- Branch from the PolicyEngine upstream repo, not a fork. Encoding work targets
origin/main of PolicyEngine/policyengine-us (etc.).
Routing
| You are working on… |
Read |
A variable: value_type/entity/defined_for, adds vs formula, naming, gotchas (age float, monthly_age, is_ssi_eligible), federal aggregators |
references/variables.md |
A parameter: YAML structure, brackets (-.inf, .inf, single_amount, right=True boundary conventions), breakdowns, metadata, path syntax |
references/parameters.md |
Periods (period vs period.this_year, ÷12) or aggregation (adds/add(), .any()) |
references/periods-and-aggregation.md |
Vectorization: where/select, max_(A-B,0) floors, divide-by-zero, phantom values |
references/vectorization.md |
| YAML tests: structure, period restrictions, error margins, enums, the CLI |
references/tests.md |
A contributed reform under contrib/ / reforms/: factory, in_effect, registration |
references/reforms.md |
| Code style, framework constants, folder layout, review response |
references/style.md |
Setup
cd policyengine-us # or -uk / -canada; branch off origin/main
uv pip install -e ".[dev]" # editable install with dev deps
uv run pytest policyengine_us/tests/... -q
uv run policyengine-core test <yaml_path> -c policyengine_us # runs a YAML test file
uv run ruff format . && uv run ruff check .
Discover names against the live tree instead of guessing — grep the variables/ and
parameters/ directories, or introspect the parameter tree in Python (see
references/parameters.md). Paths and parameter names change; the repo is the source of truth.
1---2name: policyengine-model-development3description: Load when editing a PolicyEngine country model repo (policyengine-us, policyengine-uk, policyengine-canada) — adding or changing variables, parameters, YAML tests, or contributed reforms. This is the engineering layer (how formulas, parameter YAML, and tests are written), distinct from running analysis. Triggers: add a variable, add a parameter, write a formula, defined_for, adds/subtracts, value_type, definition_period, entity, SPMUnit, TaxUnit, vectorize, where/select, period vs period.this_year, monthly_age, YAML test, absolute_error_margin, contrib reform, in_effect, create_x_reform, neutralize_variable, modify_parameters, changelog.d, StateCode, TANF/SNAP encoding, bracket parameter, -.inf, single_amount, right=True, band top, over X but not over Y. NOT for: household calculations, microsimulation, reform scoring, or distributional analysis (use the policyengine skill); calling the REST API (use policyengine-api).4---56# PolicyEngine model development78Engineering patterns for the country-model repos — `policyengine-us`, `policyengine-uk`,9`policyengine-canada`. These packages encode enacted law (and proposed reforms) as **variables**10(Python formulas) driven by **parameters** (YAML), validated by **YAML tests**, all running on11the vectorized policyengine-core engine.1213This skill is the *how-to-write-it* layer. To run calculations or score reforms, use the14`policyengine` skill instead. Verified against policyengine-us 1.764.x / policyengine-core 3.30.x15(2026-07).1617For delegated implementation, test authoring or review, follow18[agent-loading.md](references/agent-loading.md): every worker loads this entrypoint in19its own context, reads its task references and reports evidence of the load before work.2021## Mental model2223- **Parameters** are a YAML tree under `parameters/gov/...`. Each leaf is a dated value (or a24 bracket/breakdown table) with required metadata. Accessed in formulas as25 `parameters(period).gov.agency.program.thing`. Federal under `gov/{agency}/`, state under26 `gov/states/{st}/`, contributed reforms under `gov/contrib/`.27- **Variables** live one-per-file under `variables/gov/...`, each a `Variable` subclass with28 `value_type` / `entity` / `definition_period` / metadata and either a `formula` **or** an29 `adds`/`subtracts` list (never both). Formulas run vectorized over the whole population.30- **Entities** nest: `Person` → `TaxUnit` / `SPMUnit` / `Family` / `MaritalUnit` / `Household`31 (US); `Person` → `BenUnit` / `Household` (UK). Aggregation across entity levels is automatic32 via `adds`/`add()`.33- **Tests** are YAML files mirroring the variable path under `tests/policy/baseline/...`,34 asserting outputs for a described household at a period.3536## The absolute musts37381. **Never hardcode a numeric policy value in a formula.** Every threshold, rate, and amount39 comes from a parameter (`p.income_limit`, not `2000`). Bare `0`/`1`/`-1` and `MONTHS_IN_YEAR`40 are the only acceptable literals. See references/variables.md.412. **Vectorize everything.** No `if`/`elif`/`else` or `and`/`or`/`not` on entity arrays — use42 `where` / `select` / `&` / `|` / `~`. Python `if` is allowed **only** on scalar parameters43 (`if p.flat_applies:`). See references/vectorization.md.443. **`adds`/`subtracts` XOR `formula` — never both** in one variable. A pure sum uses the `adds`45 attribute with no formula; anything else uses a formula (with `add()` inside). See46 references/periods-and-aggregation.md.474. **Get the period right.** From a MONTH formula, YEAR flow variables (income) use `period`48 (auto ÷12); YEAR stocks/counts/ages/booleans use `period.this_year` (no division). See49 references/periods-and-aggregation.md.505. **`uv run` for everything.** `uv run pytest ...`, `uv run policyengine-core test ...`. Never51 bare `pytest`. Format with `uv run ruff` (line length **88**, the default — not 79). There is52 **no `black`** in this toolchain.536. **Changelog = a towncrier fragment**, never an edit to `CHANGELOG.md`. Write54 `changelog.d/{branch-name}.{added|fixed|changed}.md` with one line.557. **Branch from the PolicyEngine upstream repo, not a fork.** Encoding work targets56 `origin/main` of `PolicyEngine/policyengine-us` (etc.).5758## Routing5960| You are working on… | Read |61|---|---|62| A variable: `value_type`/`entity`/`defined_for`, `adds` vs formula, naming, gotchas (age float, `monthly_age`, `is_ssi_eligible`), federal aggregators | references/variables.md |63| A parameter: YAML structure, brackets (`-.inf`, `.inf`, `single_amount`, `right=True` boundary conventions), breakdowns, metadata, path syntax | references/parameters.md |64| Periods (`period` vs `period.this_year`, ÷12) or aggregation (`adds`/`add()`, `.any()`) | references/periods-and-aggregation.md |65| Vectorization: `where`/`select`, `max_(A-B,0)` floors, divide-by-zero, phantom values | references/vectorization.md |66| YAML tests: structure, period restrictions, error margins, enums, the CLI | references/tests.md |67| A contributed reform under `contrib/` / `reforms/`: factory, `in_effect`, registration | references/reforms.md |68| Code style, framework constants, folder layout, review response | references/style.md |6970## Setup7172```bash73cd policyengine-us # or -uk / -canada; branch off origin/main74uv pip install -e ".[dev]" # editable install with dev deps75uv run pytest policyengine_us/tests/... -q76uv run policyengine-core test <yaml_path> -c policyengine_us # runs a YAML test file77uv run ruff format . && uv run ruff check .78```7980Discover names against the live tree instead of guessing — grep the `variables/` and81`parameters/` directories, or introspect the parameter tree in Python (see82references/parameters.md). Paths and parameter names change; the repo is the source of truth.