Agent skill

policyengine-parameter-patterns

PolicyEngine parameter patterns - YAML structure, naming conventions, metadata requirements, federal/state separation

Stars 26
Forks 5

Install this agent skill to your Project

npx add-skill https://github.com/PolicyEngine/policyengine-claude/tree/main/skills/technical-patterns/policyengine-parameter-patterns-skill

SKILL.md

PolicyEngine Parameter Patterns

Comprehensive patterns for creating PolicyEngine parameter files.

Critical: Required Structure

Every parameter MUST have this exact structure:

yaml
description: [One sentence description].
values:
  YYYY-MM-DD: value

metadata:
  unit: [type]       # REQUIRED
  period: [period]   # REQUIRED
  label: [name]      # REQUIRED
  reference:         # REQUIRED
    - title: [source]
      href: [url]

Missing ANY metadata field = validation error


1. File Naming Conventions

Study Reference Implementations First

Before creating ANY parameter files, read 3-5 files from a similar program in another state. Match their structure — don't invent your own. This is the single most effective way to produce correct parameter files.

Search broadly by program concept (not just program name):

bash
# For a childcare program, search by concept keywords:
Glob 'policyengine_us/parameters/gov/states/*/*/*child*care*/'
Glob 'policyengine_us/parameters/gov/states/*/*/*ccap*/'
Glob 'policyengine_us/parameters/gov/states/*/*/*ccfa*/'

Good references by program type:

  • TANF: DC, IL, TX — /parameters/gov/states/{st}/{agency}/tanf/
  • Childcare: MA CCFA, CO CCAP — /parameters/gov/states/{st}/{agency}/ccfa/ or ccap/
  • LIHEAP: AZ — /parameters/gov/states/az/{agency}/liheap/

Naming Patterns

Dollar amounts → /amount.yaml

income/deductions/work_expense/amount.yaml     # $120
resources/limit/amount.yaml                    # $6,000
payment_standard/amount.yaml                   # $320

Percentages/rates → /rate.yaml or /percentage.yaml

income_limit/rate.yaml                         # 1.85 (185% FPL)
benefit_reduction/rate.yaml                    # 0.2 (20%)
income/disregard/percentage.yaml               # 0.67 (67%)

Thresholds → /threshold.yaml

age_threshold/minor_child.yaml                 # 18
age_threshold/elderly.yaml                     # 60
income/threshold.yaml                          # 30_000

2. Description Field

The ONLY Acceptable Formula

yaml
description: [State] [verb] [category] to [this X] under the [Full Program Name] program.

Components:

  1. [State]: Full state name (Indiana, Texas, California)
  2. [verb]: ONLY use: limits, provides, sets, excludes, deducts, uses
  3. [category]: What's being limited/provided (gross income, resources, payment standard)
  4. [this X]: ALWAYS use generic placeholder
    • this amount (for currency-USD)
    • this share or this percentage (for rates/percentages)
    • this threshold (for age/counts)
  5. [Full Program Name]: ALWAYS spell out (Temporary Assistance for Needy Families, NOT TANF)

Copy These Exact Templates

For income limits:

yaml
description: [State] limits gross income to this amount under the Temporary Assistance for Needy Families program.

For resource limits:

yaml
description: [State] limits resources to this amount under the Temporary Assistance for Needy Families program.

For payment standards:

yaml
description: [State] provides this amount as the payment standard under the Temporary Assistance for Needy Families program.

For disregards:

yaml
description: [State] excludes this share of earnings from countable income under the Temporary Assistance for Needy Families program.

Keep Descriptions Practical

Descriptions should focus on what matters for simulation, not copy regulatory language verbatim. Omit regulatory details that have no practical impact:

yaml
# ❌ Too literal — no one simulates a 1-week-old:
description: Rhode Island defines the infant/toddler age range as 1 week up to 3 years under the Child Care Assistance Program.

# ✅ Practical:
description: Rhode Island defines the infant/toddler age range as up to 3 years under the Child Care Assistance Program.

Description Validation Checklist

Run this check on EVERY description:

python
# Pseudo-code validation
def validate_description(desc):
    checks = [
        desc.count('.') == 1,  # Exactly one sentence
        'TANF' not in desc,     # No acronyms
        'SNAP' not in desc,     # No acronyms
        'this amount' in desc or 'this share' in desc or 'this percentage' in desc,
        'under the' in desc and 'program' in desc,
        'by household size' not in desc,  # No explanatory text
        'based on' not in desc,           # No explanatory text
        'for eligibility' not in desc,    # Redundant
    ]
    return all(checks)

CRITICAL: Always spell out full program names in descriptions!


3. Values Section

Format Rules

yaml
values:
  2024-01-01: 3_000    # Use underscores
  # NOT: 3000

  2024-01-01: 0.2      # Remove trailing zeros
  # NOT: 0.20 or 0.200

  2024-01-01: 2        # No decimals for integers
  # NOT: 2.0 or 2.00

Effective Dates

Use exact dates from sources:

yaml
# If source says "effective July 1, 2023"
2023-07-01: value

# If source says "as of October 1"
2024-10-01: value

# NOT arbitrary dates:
2000-01-01: value  # Shows no research

Date format: YYYY-MM-01 (always use 01 for day)

Effective Date Accuracy

Use the program's effective date, not filing or publication dates:

yaml
# Regulation filed June 5, 2003; effective January 1, 2004
❌ 2003-06-05: 500    # Filing date — WRONG
✅ 2004-01-01: 500    # Effective date — CORRECT

Beware backward extrapolation: A parameter's first entry (e.g., 2006-07-01: 20) is returned for ALL prior periods. If the program didn't exist before that date, add an explicit zero:

yaml
# ❌ Phantom $20 appears in 2005:
values:
  2006-07-01: 20

# ✅ Zero before program start:
values:
  2000-01-01: 0
  2006-07-01: 20

Don't duplicate unchanged values. If a value is the same across eras, keep one entry at the earliest applicable date.

Before removing "duplicate" date entries, verify ALL sub-categories — what looks identical at the top level may differ in specific combinations (e.g., School Age Part Time rates changed while other age/time categories stayed the same).

Flag unverified historical values in the PR description rather than silently including them.


4. Metadata Fields (ALL REQUIRED)

unit

Common units:

  • currency-USD - Dollar amounts
  • /1 - Rates, percentages (as decimals)
  • month - Number of months
  • year - Age in years
  • bool - True/false
  • person - Count of people

period

  • year - Annual values
  • month - Monthly values
  • day - Daily values
  • eternity - Never changes

Match period to the parameter's semantic meaning. A dimensionless rate or ratio should use period: year, not the period of the quantity it modifies (e.g., don't use period: week for a weekly copay rate — the rate itself is time-invariant).

label

Pattern: [State] [PROGRAM] [description]

yaml
label: Montana TANF minor child age threshold
label: Illinois TANF earned income disregard rate
label: California SNAP resource limit

Rules:

  • Spell out state name
  • Abbreviate program (TANF, SNAP)
  • No period at end

reference

Requirements:

  1. At least one source (prefer two)
  2. Must contain the actual value
  3. Title: Include FULL section path (all subsections and sub-subsections)
  4. PDF links: Add #page=XX at end of href ONLY (never in title)
  5. Verify page numbers — open the PDF and confirm the page number is correct before using it

Reference Source Hierarchy — prefer online over PDF:

  1. Online statute/regulation (best): Cornell LII, state legislature sites, public.law
    • Example: https://www.law.cornell.edu/regulations/rhode-island/218-RICR-20-00-4.6
  2. Official government HTML page: .gov pages with section anchors
  3. PDF with verified page number (last resort): Only when no online HTML version exists
    • Rate schedules, policy manuals without HTML versions
    • Always verify the #page=XX is correct

Why: Online references are linkable, searchable, and don't require page number verification. PDF page numbers are error-prone (file page vs printed page) and links break when documents are updated.

Title Format - Include ALL subsection levels (NO page numbers):

yaml
# ❌ BAD - Too generic:
title: OAR 461-155  # Missing subsections!
title: Section 5    # Which subsection?
title: TEA Manual, page 13  # Page number belongs in href, not title!

# ✅ GOOD - Full section path, no page number:
title: OAR 461-155-0030(2)(a)(B)     # All levels included
title: 7 CFR § 273.9(d)(6)(ii)(A)    # Federal regulation with all subsections
title: Indiana Admin Code 12-14-2-3.5(b)(1)  # State admin code
title: Arkansas TEA Manual Section 5.2.3    # Manual with section (page in href)

PDF Link Format - Always include page in href:

CRITICAL: Use the PDF file page number, NOT the printed page number inside the document.

  • The #page=XX value is the page position in the PDF file (1st page = 1, 2nd page = 2, etc.)
  • This may differ from the page number printed on the document itself
  • Why? When users click the link, they must land directly on the page showing the referenced values
yaml
# ❌ BAD - No page number:
href: https://state.gov/manual.pdf

# ✅ GOOD - Page anchor in href (file page number):
href: https://humanservices.arkansas.gov/wp-content/uploads/TEA_MANUAL.pdf#page=13
href: https://adminrules.idaho.gov/rules/current/16/160503.pdf#page=8

Complete Examples:

yaml
✅ GOOD (page number in href only):
reference:
  - title: OAR 461-155-0030(2)(a)(B)
    href: https://oregon.public.law/rules/oar_461-155-0030
  - title: Oregon DHS TANF Policy Manual Section 4.3.2
    href: https://oregon.gov/dhs/tanf-manual.pdf#page=23

✅ GOOD:
reference:
  - title: 7 CFR § 273.9(d)(6)(ii)(A)
    href: https://www.ecfr.gov/current/title-7/section-273.9#p-273.9(d)(6)(ii)(A)
  - title: Arkansas TEA Manual Section 2100
    href: https://humanservices.arkansas.gov/wp-content/uploads/TEA_MANUAL.pdf#page=45

❌ BAD (page number in title):
reference:
  - title: Arkansas TEA Manual, page 13  # Page belongs in href!
    href: https://humanservices.arkansas.gov/wp-content/uploads/TEA_MANUAL.pdf

❌ BAD (missing info):
reference:
  - title: Federal LIHEAP regulations  # Too generic - no section!
    href: https://www.acf.hhs.gov/ocs  # No specific page
  - title: OAR 461-155  # Missing subsections (2)(a)(B)!
    href: https://oregon.gov/manual.pdf  # Missing #page=XX

Reference Verification Rules

Distinguish statute from regulation. Cite the statute that establishes a rule and the regulation that implements it separately — they are different legal instruments.

Prefer direct document URLs over landing-page or index-page URLs. Agency websites frequently reorganize navigation pages while direct document links remain stable.

Annotate reference coverage windows. When a source has a known publication cutoff (e.g., last published in 2011), note which values it corroborates — don't cite it for values postdating the cutoff.

Secondary sources (WorkWorld, SSA reports) are simplified. They may omit eligibility requirements, payment categories, or add interpretive language. Always treat the Admin Code/state regulation as authoritative; use secondary sources for cross-verification only.

Verify regulation text is present. When fetching from legal databases (Cornell LII, Westlaw), verify the content contains actual regulation body text, not just a JavaScript shell — many databases render client-side.

When a regulation portal migrates domains, update ALL parameter files referencing the old domain in one pass — broken redirects are systematic.

For state policy choices, cite the state regulation, not the federal CFR that merely sets the ceiling.


5. Federal/State Separation

Federal Parameters

Location: /parameters/gov/{agency}/{program}/

yaml
# parameters/gov/hhs/fpg/first_person.yaml
description: HHS sets this amount as the federal poverty guideline for one person.

State Parameters

Location: /parameters/gov/states/{state}/{agency}/{program}/

yaml
# parameters/gov/states/ca/dss/tanf/income_limit/rate.yaml
description: California uses this multiplier of the federal poverty guideline for TANF income eligibility.

5.5 Parameter Folder Organization

Core Principles

  1. Group logically - Parameters that relate to the same aspect should be together
  2. Don't create subfolder for 1 file - If only 1 parameter for an aspect, keep it at parent level
  3. Payment standard at root - Main benefit amounts can stay at program root

Common Aspects (adapt to your program)

  • income/ - Income limits, deductions, disregards
  • eligibility/ - Age thresholds, citizenship requirements
  • resources/ - Asset/resource limits

Study Existing Implementations

Each program is different. Before organizing, look at similar programs:

bash
ls policyengine_us/parameters/gov/states/{state}/{agency}/

6. Common Parameter Patterns

Income Limits (as FPL multiplier)

yaml
# income_limit/rate.yaml
description: State uses this multiplier of the federal poverty guideline for program income limits.
values:
  2024-01-01: 1.85  # 185% FPL

metadata:
  unit: /1
  period: year
  label: State PROGRAM income limit multiplier

Benefit Amounts

yaml
# payment_standard/amount.yaml
description: State provides this amount as the monthly program benefit.
values:
  2024-01-01: 500

metadata:
  unit: currency-USD
  period: month
  label: State PROGRAM payment standard amount

Age Thresholds (Simple)

yaml
# age_threshold/minor_child.yaml
description: State defines minor children as under this age for program eligibility.
values:
  2024-01-01: 18

metadata:
  unit: year
  period: eternity
  label: State PROGRAM minor child age threshold

Age-Based Eligibility (Bracket Style) - PREFERRED

When eligibility depends on age ranges, use a single bracket-style parameter instead of separate min/max files.

yaml
# eligibility/by_age.yaml
description: Massachusetts determines eligibility for the Bay Transportation reduced fare program based on age.

metadata:
  threshold_unit: year
  amount_unit: bool
  period: year
  type: single_amount
  label: Massachusetts Bay Transportation reduced fare age eligibility
  reference:
    - title: MBTA Reduced Fare Program
      href: https://www.mbta.com/fares/reduced

brackets:
  - threshold:
      2024-01-01: 0
    amount:
      2024-01-01: false    # Under 18: not eligible
  - threshold:
      2024-01-01: 18
    amount:
      2024-01-01: true     # Ages 18-64: eligible
  - threshold:
      2024-01-01: 65
    amount:
      2024-01-01: false    # 65+: not eligible (different program)

Federal example (SNAP student eligibility):

yaml
# parameters/gov/usda/snap/student_age_eligibility_threshold.yaml
description: The United States includes students in this age range for SNAP eligibility.

brackets:
  - threshold:
      2018-01-01: 0
    amount:
      2018-01-01: true     # Under 18: eligible
  - threshold:
      2018-01-01: 18
    amount:
      2018-01-01: false    # Ages 18-49: not eligible (student restrictions)
  - threshold:
      2018-01-01: 50
    amount:
      2018-01-01: true     # 50+: eligible

metadata:
  type: single_amount
  threshold_unit: year
  amount_unit: bool
  label: SNAP student age eligibility threshold
  reference:
    - title: 7 U.S. Code § 2015 - Eligibility disqualifications
      href: https://www.law.cornell.edu/uscode/text/7/2015

When to use bracket-style:

  • ✅ Eligibility varies by age range (eligible for ages X-Y only)
  • ✅ Multiple age cutoffs affect the same benefit
  • ✅ Boolean eligibility that changes at different thresholds
  • ✅ Non-contiguous eligibility (e.g., eligible under 18 AND over 50, but not 18-49)

When NOT to use bracket-style:

  • ❌ Single threshold (just use simple threshold.yaml)
  • ❌ Non-boolean values that scale with age (use single_amount brackets with currency amounts)

Disregard Percentages

yaml
# income/disregard/percentage.yaml
description: State excludes this share of earned income from program calculations.
values:
  2024-01-01: 0.67  # 67%

metadata:
  unit: /1
  period: eternity
  label: State PROGRAM earned income disregard percentage

Bracket-Based Parameters

CRITICAL: Handling Negative Values

When creating bracket-based parameters (e.g., tax credits based on AGI), the first bracket threshold MUST be -.inf if negative values are possible, NOT 0.

❌ WRONG - Excludes negative AGI:

yaml
# threshold.yaml (for single filers)
brackets:
  - threshold:
      2023-01-01: 0      # ❌ Bug: negative AGI excluded!
    amount:
      2023-01-01: 300
  - threshold:
      2023-01-01: 30_000
    amount:
      2023-01-01: 110

✅ CORRECT - Includes all possible values:

yaml
# threshold.yaml (for single filers)
brackets:
  - threshold:
      2023-01-01: -.inf  # ✅ Covers negative AGI
    amount:
      2023-01-01: 300
  - threshold:
      2023-01-01: 30_000
    amount:
      2023-01-01: 110

When to use -.inf:

  • Income-based calculations (AGI can be negative)
  • Any parameter where negative input values are valid
  • Tax credits, deductions, or benefits based on earnings

When 0 is appropriate:

  • Age thresholds (always non-negative)
  • Count-based parameters (household size, number of dependents)
  • Resource limits (assets can't be negative)

Real-world example: Hawaii Food/Excise Tax Credit uses AGI brackets. The first threshold must be -.inf to correctly handle taxpayers with negative AGI (e.g., business losses).

Bracket Boundary: "Above X%" Regulations

CRITICAL: PolicyEngine's single_amount bracket uses "at or above threshold" logic. A value exactly at the threshold gets that bracket's rate, not the previous bracket's. When a regulation says "above X%" (meaning X% itself belongs to the lower bracket), shift the threshold by 0.0001 to match.

Example — co-payment tiers by FPL:

Regulation says:
  Level 0: Less than or equal to 100% → $0
  Level 1: Above 100% up to and including 125% → 2%
  Level 2: Above 125% up to and including 150% → 5%
  Level 3: Above 150% up to and including 261% → 7%

❌ WRONG — family at exactly 100% FPL gets 2% instead of 0%:

yaml
brackets:
  - threshold:
      2024-01-01: 0
    amount:
      2024-01-01: 0
  - threshold:
      2024-01-01: 1.0    # ❌ At 100% FPL → hits this bracket
    amount:
      2024-01-01: 0.02

✅ CORRECT — shift by 0.0001 to encode "above X%":

yaml
brackets:
  - threshold:
      2024-01-01: 0
    amount:
      2024-01-01: 0
  - threshold:
      2024-01-01: 1.0001  # ✅ "Above 100%" → 100% stays in previous bracket
    amount:
      2024-01-01: 0.02
  - threshold:
      2024-01-01: 1.2501  # ✅ "Above 125%"
    amount:
      2024-01-01: 0.05
  - threshold:
      2024-01-01: 1.5001  # ✅ "Above 150%"
    amount:
      2024-01-01: 0.07

BOUNDARY CHECK — do this for every bracket parameter:

  1. Read the regulation's exact wording for each threshold
  2. If it says "above X" or "more than X" → shift by 0.0001
  3. If it says "at or above X" or "X or more" → no shift needed
  4. Apply consistently to ALL thresholds in the bracket

When to apply the 0.0001 shift:

  • Regulation says "above X%" or "more than X%" (exclusive of the boundary)
  • Apply consistently to ALL thresholds in the bracket, not just the first

When NOT to shift:

  • Regulation says "at or above X%" or "X% or more" (inclusive — matches PolicyEngine's default)
  • Regulation says "at least X%" (inclusive)

Multi-Dimensional Rate Tables (Enum Breakdowns)

When a rate table has multiple dimensions (e.g., age group × star rating × authorization level), use Enum breakdowns in a small number of parameter files — NOT one file per combination.

❌ BAD — 45 files (one per age × time level):

rates/licensed_center/infant/full_time.yaml      # breakdown: [star_rating]
rates/licensed_center/infant/half_time.yaml
rates/licensed_center/toddler/full_time.yaml
... (16 files for center alone, 45 total)

✅ GOOD — 3 files (one per provider type) with multi-dimensional Enum breakdowns:

rates/licensed_center.yaml     # breakdown: [time_category, star_rating, age_group]
rates/licensed_family.yaml     # breakdown: [time_category, star_rating, age_group]
rates/license_exempt.yaml      # breakdown: [time_category, step_rating, age_group]

Example rate file with 3D Enum breakdown:

yaml
# rates/licensed_center.yaml
description: Rhode Island provides these weekly reimbursement rates for licensed child care centers under the Child Care Assistance Program.
metadata:
  period: week
  unit: currency-USD
  label: Rhode Island CCAP licensed center weekly rates
  breakdown:
    - ri_ccap_time_category
    - ri_ccap_star_rating
    - ri_ccap_center_age_group
  reference:
    - title: 218-RICR-20-00-4.12
      href: https://www.law.cornell.edu/regulations/rhode-island/218-RICR-20-00-4.12

FULL_TIME:
  STAR_1:
    INFANT:
      2025-07-01: 334
    TODDLER:
      2025-07-01: 278
    PRESCHOOL:
      2025-07-01: 236
    SCHOOL_AGE:
      2025-07-01: 210
  STAR_2:
    INFANT:
      2025-07-01: 341
    ...

The variable lookup becomes simple Enum indexing:

python
p.licensed_center[time_category][star_rating][center_age]

When dimensions differ by category (e.g., licensed centers have 4 age groups, family care has 3):

  • Use separate Enum variables per category (ri_ccap_center_age_group, ri_ccap_family_age_group)
  • Each rate file references the appropriate Enum in its breakdown
  • The variable uses select() on the top-level category (provider type) — each branch uses its own Enums

When quality ratings differ (e.g., star ratings 1-5 for licensed, step ratings 1-4 for exempt):

  • Use separate Enum variables (ri_ccap_star_rating, ri_ccap_step_rating)
  • Each rate file references the appropriate Enum

Rule of thumb: If you need helper functions in the variable to do the rate lookup, your parameter structure is too granular. Restructure the parameters.

Don't Create Unnecessary Parameters

Don't create parameters for:

  • Universal conversion factors — Use framework constants instead. WEEKS_IN_YEAR / MONTHS_IN_YEAR gives weeks-per-month. MONTHS_IN_YEAR gives 12. Don't create a weeks_per_month.yaml parameter.
  • Thresholds with no practical simulation impact — A minimum age of "1 week" for childcare has no simulation value. No one models newborns. Skip it.
  • Values derivable from existing parameters — If FPL tables already exist, don't recreate them as program-specific parameters.

Parameter Structure Transitions (Flat → Bracket)

When a parameter changes structure over time (e.g., a flat rate becomes a tiered/marginal rate in a later year), you CANNOT put both structures in a single YAML file. Instead, split into separate files with a boolean toggle.

Problem: A single rate.yaml with marginal brackets would retroactively apply the tiered structure to years that had a flat rate.

Solution: Create a rate/ folder with three files:

rate/
├── flat.yaml           # The original flat-rate value
├── incremental.yaml    # The new bracket/marginal structure
└── flat_applies.yaml   # Boolean toggle: true = use flat, false = use brackets

rate/flat.yaml — The original single-value parameter:

yaml
description: Washington taxes long-term capital gains at this rate.
values:
  2022-01-01: 0.07

metadata:
  unit: /1
  period: year
  label: Washington flat capital gains tax rate
  reference:
    - title: RCW 82.87.040(1) Tax imposed—Long-term capital assets
      href: https://app.leg.wa.gov/RCW/default.aspx?cite=82.87.040

rate/incremental.yaml — The new bracket structure:

yaml
description: Washington taxes long-term capital gains at these marginal rates.
brackets:
  - threshold:
      2022-01-01: 0
    rate:
      2022-01-01: 0.07
  - threshold:
      2025-01-01: 1_000_000
    rate:
      2025-01-01: 0.099

metadata:
  threshold_unit: currency-USD
  rate_unit: /1
  threshold_period: year
  type: marginal_rate
  label: Washington marginal capital gains tax rate
  reference:
    - title: RCW 82.87.040(1)-(2) Tax imposed—Long-term capital assets
      href: https://app.leg.wa.gov/RCW/default.aspx?cite=82.87.040
    - title: ESSB 5813, Chapter 421, Laws of 2025, Sec. 101
      href: https://lawfilesext.leg.wa.gov/biennium/2025-26/Htm/Bills/Session%20Laws/Senate/5813-S.SL.htm

rate/flat_applies.yaml — The boolean toggle:

yaml
description: Washington uses this indicator to determine whether the flat capital gains tax rate applies.
values:
  2022-01-01: true
  2025-01-01: false

metadata:
  unit: bool
  period: year
  label: Washington flat capital gains tax rate applies
  reference:
    - title: RCW 82.87.040(1) Tax imposed—Long-term capital assets
      href: https://app.leg.wa.gov/RCW/default.aspx?cite=82.87.040
    - title: ESSB 5813, Chapter 421, Laws of 2025, Sec. 101
      href: https://lawfilesext.leg.wa.gov/biennium/2025-26/Htm/Bills/Session%20Laws/Senate/5813-S.SL.htm

When to use this pattern:

  • ✅ A flat rate becomes a marginal bracket schedule
  • ✅ A single value becomes a lookup table by household size
  • ✅ Any parameter whose YAML structure type changes at a specific date

When NOT to use this pattern:

  • ❌ Values change but the structure stays the same (just add a new date entry)
  • ❌ A new bracket is added to an existing bracket structure (see below)

See variable patterns skill for the corresponding variable-side logic (if p.rate.flat_applies).

Adding New Brackets to Existing Scales

When a new bracket is added to an existing scale in a later year, you CANNOT simply add the bracket with only the new year's date — the bracket would have no defined threshold/amount for prior years, breaking the scale.

Solution: Add the new bracket with its threshold set to .inf (or -.inf for rate brackets starting from the bottom) for the base year. This makes the bracket structurally present for all years but functionally unreachable before the year it takes effect.

Example: Ohio personal exemption phase-out (HB 96)

Ohio's personal exemption had 3 brackets (by AGI). Starting 2025, a 4th bracket phases the exemption to $0 at high incomes ($750k in 2025, $500k in 2026+):

yaml
brackets:
  - threshold:
      2021-01-01: 0
    amount:
      2021-01-01: 2_400
  - threshold:
      2021-01-01: 40_001
    amount:
      2021-01-01: 2_150
  - threshold:
      2021-01-01: 80_001
    amount:
      2021-01-01: 1_900
  # New bracket: use .inf for base year so pre-2025 is unaffected
  - threshold:
      2021-01-01: .inf         # Pre-2025: unreachable → bracket is inert
      2025-01-01: 750_000      # 2025: phase-out at $750k
      2026-01-01: 500_000      # 2026+: phase-out drops to $500k
    amount:
      2021-01-01: 0            # $0 exemption above threshold

Why .inf works:

  • For pre-2025 periods, no income can reach .inf, so the bracket never activates
  • Starting 2025, the threshold becomes a real value ($750k) and the bracket takes effect
  • The scale remains structurally valid across all time periods

Another example: Ohio joint filing credit MAGI cap

yaml
brackets:
  - threshold:
      2021-01-01: 0
    amount:
      2021-01-01: 0.2
  - threshold:
      2021-01-01: 25_000
    amount:
      2021-01-01: 0.15
  - threshold:
      2021-01-01: 50_000
    amount:
      2021-01-01: 0.1
  - threshold:
      2021-01-01: 75_000
    amount:
      2021-01-01: 0.05
  # New bracket: MAGI cap added by HB 96
  - threshold:
      2021-01-01: .inf
      2025-01-01: 750_000
      2026-01-01: 500_000
    amount:
      2021-01-01: 0

When to use .inf for new brackets:

  • ✅ A new upper bracket is added in a later year (cap, phase-out, new rate tier)
  • ✅ The bracket should not affect calculations for prior years
  • ✅ The new bracket sets a value to zero (phase-out) or introduces a new rate

When NOT to use this pattern:

  • ❌ The bracket existed in all prior years too (just add it normally with the base date)
  • ❌ The parameter structure type itself changes (use the flat→bracket transition pattern above)

Real-world reference: policyengine-us PR #7107 — Ohio 2025 income tax update (HB 96 personal exemption and joint filing credit MAGI caps).

Choosing Between the Three Boolean Toggle Approaches

The flat→bracket transition, .inf new bracket, and in_effect provision gating patterns all handle parameters that change over time, but they solve different problems:

Flat→Bracket Transition .inf New Bracket in_effect Provision Gating
Problem Structure type changes (flat → brackets) New bracket added to existing scale A provision starts or ends at a specific date
Parameter side Split into folder + boolean toggle Add bracket with .inf threshold Single in_effect.yaml boolean
Variable side if p.toggle: to choose access method No changes — .calc() works if p.in_effect: gates entire logic block
Example WA capital gains: flat 7% → tiered 7%/9.9% OH exemptions: 3→4 brackets CT TFA high earnings reduction (new in 2024)

Provision Gating with in_effect Boolean

When a provision starts (or ends) at a specific date, create a boolean parameter that gates the entire logic block in the variable formula. This is different from flat_applies — it doesn't switch between two parameter access methods, it controls whether a block of logic runs at all.

Use case: A new program feature is added by legislation (e.g., a high-earnings reduction that didn't exist before 2024), or an existing feature is repealed.

in_effect.yaml — Boolean that tracks when the provision is active:

yaml
# e.g., payment/high_earnings/in_effect.yaml
description: Connecticut uses this indicator to determine whether the high-earnings benefit reduction applies under the Temporary Family Assistance program.

values:
  1997-01-01: false
  2024-01-01: true

metadata:
  unit: bool
  period: month
  label: Connecticut TFA high earnings reduction in effect
  reference:
    - title: State of Connecticut TANF State Plan 2024-2026, High Earnings Provision
      href: https://portal.ct.gov/dss/-/media/departments-and-agencies/dss/state-plans-and-federal-reports/tanf-state-plan/ct-tanf-state-plan-2024---2026---41524-amendment.pdf#page=10
    - title: Connecticut General Statutes § 17b-112(d)
      href: https://cga.ct.gov/current/pub/chap_319s.htm#sec_17b-112

Sibling parameters — The provision's actual values live alongside in_effect.yaml:

payment/high_earnings/
├── in_effect.yaml      # false before 2024, true from 2024
├── rate.yaml            # FPL multiplier threshold (e.g., 0.75)
└── reduction_rate.yaml  # Benefit reduction rate (e.g., 0.25)

See variable patterns skill for the corresponding variable-side logic (if p.high_earnings.in_effect:).

When to use this pattern:

  • ✅ A new provision is added by legislation at a specific date
  • ✅ An existing provision is repealed at a specific date
  • ✅ The provision gates an entire block of logic (not just a parameter access method)
  • ✅ The provision has its own sub-parameters (rates, thresholds) that only make sense when active

When NOT to use this pattern:

  • ❌ The parameter structure itself changes (use flat→bracket transition)
  • ❌ A new bracket is added to an existing scale (use .inf pattern)
  • ❌ A simple value changes over time (just add a new date entry)

Regional Variation with regional_in_effect Boolean

When a program has regional payment variations that start or end at a specific date, create a boolean that switches between regional lookup and a flat statewide amount.

Use case: A state originally had different payment standards by region, then consolidated to a single statewide amount (or vice versa).

regional_in_effect.yaml — Boolean that tracks when regional variation applies:

yaml
# e.g., payment/regional_in_effect.yaml
description: Connecticut uses this indicator to determine whether regional payment standards apply under the Temporary Family Assistance program.

values:
  1997-01-01: true
  2022-07-01: false

metadata:
  unit: bool
  period: month
  label: Connecticut TFA regional payment standards in effect
  reference:
    - title: Connecticut General Statutes § 17b-104(c)
      href: https://cga.ct.gov/current/pub/chap_319s.htm#sec_17b-104
    - title: State of Connecticut TANF State Plan 2021-2023
      href: https://portal.ct.gov/dss/-/media/departments-and-agencies/dss/state-plans-and-federal-reports/tanf-state-plan/ct-tanf-plan-2021-2023--draft.pdf#page=57

Folder structure — Regional amounts AND the flat statewide amount coexist:

payment/
├── regional_in_effect.yaml          # true before 2022-07, false after
├── regional/
│   ├── region_a/amount.yaml         # Regional amounts (by household size)
│   ├── region_b/amount.yaml
│   └── region_c/amount.yaml
├── amount.yaml                      # Flat statewide amount (used when regional_in_effect is false)
└── max_unit_size.yaml

See variable patterns skill for the corresponding variable-side logic (if p.regional_in_effect:).

When to use this pattern:

  • ✅ A program transitions from regional to statewide payment standards (or vice versa)
  • ✅ Regional variation is controlled by legislation at a specific date
  • ✅ The regional and flat structures are fundamentally different (enum lookup vs simple index)

When NOT to use this pattern:

  • ❌ Regional variation always applies (just use the regional parameters directly)
  • ❌ The variation is by household characteristic, not geographic region (use where() in variable)

Real-world reference: Connecticut TFA payment standards — regional (Region A/B/C) before July 2022, flat statewide amount after.


6.5 Bracket parameter path syntax (for reforms and Python access)

CRITICAL: When referencing bracket/scale parameters in reform dicts or Python code, the bracket index goes directly on the scale node, NOT on a .brackets sub-path.

The YAML file defines brackets as a list, but the parameter tree flattens them. The bracket index attaches to the node that contains the brackets list, not to a child called brackets.

Correct syntax

python
# Tax bracket rates — index on the scale node directly
"gov.states.ca.tax.income.rates.single[8].rate"
"gov.states.ca.tax.income.rates.single[8].threshold"

# UK income tax rates
"gov.hmrc.income_tax.rates.uk[0].rate"
"gov.hmrc.income_tax.rates.uk[1].threshold"

# CTC amount (bracket-based parameter)
"gov.irs.credits.ctc.amount.base[0].amount"

# EITC phase-out thresholds
"gov.irs.credits.eitc.phase_out.start[0].amount"

Wrong syntax (common mistake)

python
# ❌ WRONG — there is no ".brackets" in the path
"gov.states.ca.tax.income.rates.single.brackets[8].rate"
"gov.irs.credits.ctc.amount.base.brackets[0].amount"

# ❌ WRONG — missing bracket index entirely
"gov.states.ca.tax.income.rates.single.rate"
"gov.irs.credits.ctc.amount.base.amount"

How to determine the correct path

  1. Find the YAML file in the parameters directory (e.g., parameters/gov/states/ca/tax/income/rates/single.yaml)
  2. The parameter path is the directory path with dots, ending at the YAML filename (without .yaml)
  3. Add the bracket index directly: path.to.scale_file[N].rate or path.to.scale_file[N].threshold
  4. Verify in Python:
    python
    from policyengine_us import CountryTaxBenefitSystem
    p = CountryTaxBenefitSystem().parameters
    # Navigate to the node and check:
    print(p.gov.irs.credits.ctc.amount.base[0].amount("2026-01-01"))
    

Using bracket paths in Reform.from_dict()

python
from policyengine_core.reforms import Reform

# ✅ Correct
reform = Reform.from_dict({
    'gov.irs.credits.ctc.amount.base[0].amount': {
        '2026-01-01.2100-12-31': 3000
    },
    'gov.states.ca.tax.income.rates.single[8].rate': {
        '2026-01-01.2100-12-31': 0.143
    },
}, 'policyengine_us')

# ❌ Wrong — will fail or silently not apply
reform = Reform.from_dict({
    'gov.irs.credits.ctc.amount.base.brackets[0].amount': {
        '2026-01-01.2100-12-31': 3000
    },
}, 'policyengine_us')

7. Transcription & Verification

Verify Individual Values

When transcribing rate tables, verify each individual value against the source PDF. Single-digit transpositions (e.g., 305 vs 355) cascade to all derived values.

Don't Assume Arithmetic Ratios

Never assume fractional-time rates are simple fractions of full-time (e.g., half-time ≠ FT/2). Always verify against the official published rate schedule — actual ratios may differ significantly.

Implement All Rows

Always implement ALL rows from the regulatory payment table, not just those in secondary sources. WorkWorld and SSA reports may omit categories that exist in the Admin Code.

Resolve Source Conflicts

  • Body text vs data table: When a regulation's body text states one value but the table implies a different one, compute from the table — body text may reflect outdated policy.
  • State plan vs operational document: When they appear to conflict, investigate whether they answer different questions (e.g., "max for one child" vs "max for any family"); the operational document is the implementation source of truth.
  • "Maximum" rates: When a state plan reports a single maximum, verify whether it applies to all household configurations or only a subset.

Parameter Framework Gotcha

When the parameter framework caches the root parameter tree during microsim init, parameters with start dates after the test year cause ParameterNotFoundError. Use the program's actual policy effective date, not a regulation amendment filing date.

Didn't find tool you were looking for?

Be as detailed as possible for better results