Policyengine model development
Skill PolicyEngine/policyengine-claude/skills/policyengine-model-development
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. NOT for: household calculations, microsimulation, reform scoring, or distributional analysis (use the policyengine skill); calling the REST API (use policyengine-api).From its SKILL.md
npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-model-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
5.5 KB, as published. Nobody here has run it
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).
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 asparameters(period).gov.agency.program.thing. Federal undergov/{agency}/, state undergov/states/{st}/, contributed reforms undergov/contrib/. - Variables live one-per-file under
variables/gov/..., each aVariablesubclass withvalue_type/entity/definition_period/ metadata and either aformulaor anadds/subtractslist (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 viaadds/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, not2000). Bare0/1/-1andMONTHS_IN_YEARare the only acceptable literals. See references/variables.md. - Vectorize everything. No
if/elif/elseorand/or/noton entity arrays — usewhere/select/&/|/~. Pythonifis allowed only on scalar parameters (if p.flat_applies:). See references/vectorization.md. adds/subtractsXORformula— never both in one variable. A pure sum uses theaddsattribute with no formula; anything else uses a formula (withadd()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 useperiod.this_year(no division). See references/periods-and-aggregation.md. uv runfor everything.uv run pytest ...,uv run policyengine-core test .... Never barepytest. Format withuv run ruff(line length 88, the default — not 79). There is noblackin this toolchain.- Changelog = a towncrier fragment, never an edit to
CHANGELOG.md. Writechangelog.d/{branch-name}.{added|fixed|changed}.mdwith one line. - Branch from the PolicyEngine upstream repo, not a fork. Encoding work targets
origin/mainofPolicyEngine/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), 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.