| name | policyengine-variable-patterns |
| description | PolicyEngine variable patterns - variable creation, no hard-coding principle, federal/state separation, metadata standards |
PolicyEngine Variable Patterns
Essential patterns for creating PolicyEngine variables for government benefit programs.
FIRST PRINCIPLE: Legal Code is the Source of Truth
The law defines WHAT to implement. These patterns are just HOW to implement it.
1. READ the legal code/policy manual FIRST
2. UNDERSTAND what the law actually says
3. IMPLEMENT exactly what the law requires
4. USE these patterns as tools to implement correctly
Patterns are tools, not rules to blindly follow:
- If the legal code says something different from common patterns → FOLLOW THE LAW
- If another state does it differently → Check YOUR state's legal code
- If a pattern doesn't fit the regulation → Implement what the law says
Every implementation decision should trace back to a specific legal citation.
PolicyEngine Architecture Constraints
What CANNOT Be Simulated (Single-Period Limitation)
CRITICAL: PolicyEngine uses single-period simulation architecture
The following CANNOT be implemented and should be SKIPPED when found in documentation:
1. Time Limits and Lifetime Counters
Cannot simulate:
- ANY lifetime benefit limits (X months total)
- ANY time windows (X months within Y period)
- Benefit clocks and countable months
- Cumulative time tracking
Why: Requires tracking benefit history across multiple periods. PolicyEngine simulates one period at a time with no state persistence.
What to do: Document in comments but DON'T parameterize or implement:
2. Work History Requirements
Cannot simulate:
- "Must have worked 6 of last 12 months"
- "Averaged 30 hours/week over past quarter"
- Prior employment verification
- Work participation rate tracking
Why: Requires historical data from previous periods.
3. Waiting Periods and Benefit Delays
Cannot simulate:
- "3-month waiting period for new residents"
- "Benefits start month after application"
- Retroactive eligibility
- Benefit recertification cycles
Why: Requires tracking application dates and eligibility history.
4. Progressive Sanctions and Penalties
Cannot simulate:
- "First violation: 1-month sanction, Second: 3-month, Third: permanent"
- Graduated penalties
- Strike systems
Why: Requires tracking violation history.
5. Asset Spend-Down Over Time
Cannot simulate:
- Medical spend-down across months
- Resource depletion tracking
- Accumulated medical expenses
Why: Requires tracking expenses and resources across periods.
What CAN Be Simulated (With Caveats)
PolicyEngine CAN simulate point-in-time eligibility and benefits:
- ✅ Current month income limits
- ✅ Current month resource limits
- ✅ Current benefit calculations
- ✅ Current household composition
- ✅ Current deductions and disregards
Time-Limited Benefits That Affect Current Calculations
Special Case: Time-limited deductions/disregards
When a deduction or disregard is only available for X months:
- DO implement the deduction (assume it applies)
- DO add a comment explaining the time limitation
- DON'T try to track or enforce the time limit
Example:
class state_tanf_countable_earned_income(Variable):
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.xx.tanf.income
earned = spm_unit("tanf_gross_earned_income", period)
disregard_rate = p.earned_income_disregard_rate
return earned * (1 - disregard_rate)
Rule: If it requires history or future tracking, it CANNOT be fully simulated - but implement what we can and document limitations
Critical Principles
1. ZERO Hard-Coded Values
Every numeric value MUST be parameterized
❌ FORBIDDEN:
return where(eligible, 1000, 0)
age < 15
benefit = income * 0.33
month >= 10 and month <= 3
✅ REQUIRED:
return where(eligible, p.maximum_benefit, 0)
age < p.age_threshold.minor_child
benefit = income * p.benefit_rate
month >= p.season.start_month
Acceptable literals:
0, 1, -1 for basic math
12 for month conversion (/ 12, * 12)
- Array indices when structure is known
2. No Placeholder Implementations
Delete the file rather than leave placeholders
❌ NEVER:
def formula(entity, period, parameters):
return 75
✅ ALWAYS:
3. Use adds or add() - NEVER Manual Addition
CRITICAL: NEVER manually fetch variables and add them with +. Always use adds or add().
Rule 1: Pure sum → adds attribute (no formula)
❌ WRONG - Writing a formula for simple sum:
class tx_tanf_gross_income(Variable):
def formula(spm_unit, period, parameters):
earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
return earned + unearned
✅ CORRECT - Use adds, no formula needed:
class tx_tanf_gross_income(Variable):
value_type = float
entity = SPMUnit
definition_period = MONTH
adds = ["tanf_gross_earned_income", "tanf_gross_unearned_income"]
Rule 2: Sum + other operations → add() function
❌ WRONG - Manual fetching and adding:
def formula(spm_unit, period, parameters):
earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
gross = earned + unearned
return gross * p.rate
✅ CORRECT - Use add() function:
def formula(spm_unit, period, parameters):
gross = add(spm_unit, period, ["tanf_gross_earned_income", "tanf_gross_unearned_income"])
return gross * p.rate
Decision rule:
- Is it ONLY a sum? →
adds = [...] (no formula)
- Sum + other operations? →
add() function inside formula
See policyengine-aggregation-skill for detailed patterns.
Variable Implementation Standards
Variable Metadata Format
Follow established patterns:
class il_tanf_countable_earned_income(Variable):
value_type = float
entity = SPMUnit
definition_period = MONTH
label = "Illinois TANF countable earned income"
unit = USD
reference = "https://www.law.cornell.edu/regulations/illinois/..."
defined_for = StateCode.IL
adds = ["il_tanf_earned_income_after_disregard"]
Key rules:
- ✅ Use full URL in
reference (clickable)
- ✅ For PDF links, include page number:
#page=XX
- ✅ For multiple references, use TUPLE
() not list []
- ❌ Don't use
documentation field - use reference instead
- ❌ Don't use statute citations without URLs
❌ WRONG - Don't use documentation field:
class some_variable(Variable):
documentation = "This is the wrong field"
✅ CORRECT - Use reference field:
class some_variable(Variable):
reference = "https://example.gov/rules.pdf#page=10"
Reference format:
reference = "https://oregon.gov/dhs/tanf-manual.pdf#page=23"
reference = (
"https://oregon.public.law/rules/oar_461-155-0030",
"https://oregon.gov/dhs/tanf-manual.pdf#page=23",
)
reference = [
"https://...",
"https://...",
]
When to Use adds vs formula
CRITICAL: Never use both adds/subtracts AND a custom formula in the same variable!
This causes bugs when the two get out of sync. Choose one approach:
❌ FORBIDDEN - Mixing compositional and formula:
class household_net_income(Variable):
subtracts = ["employee_pension_contributions"]
def formula(household, period):
gross = household("household_gross_income", period)
tax = household("income_tax", period)
return gross - tax
Use adds/subtracts when:
- Just summing variables
- Passing through a single variable
- No transformations needed
✅ BEST - Pure compositional:
class tanf_gross_income(Variable):
adds = ["employment_income", "self_employment_income"]
✅ BEST - Compositional with subtracts:
class household_net_income(Variable):
adds = ["household_gross_income"]
subtracts = ["income_tax", "employee_pension_contributions"]
Use formula when:
- Applying transformations
- Conditional logic
- Calculations needed
✅ CORRECT - Pure formula:
def formula(entity, period, parameters):
income = add(entity, period, ["income1", "income2"])
return max_(0, income)
TANF Countable Income Pattern
Critical: Verify Calculation Order from Legal Code
MOST IMPORTANT: Always check the state's legal code or policy manual for the exact calculation order. The pattern below is typical but not universal.
The Typical Pattern:
- Apply deductions/disregards to earned income only
- Use
max_() to prevent negative earned income
- Add unearned income (which typically has no deductions)
This pattern is based on how MOST TANF programs work, but you MUST verify with the specific state's legal code.
❌ WRONG - Applying deductions to total income
def formula(spm_unit, period, parameters):
gross_earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
deductions = spm_unit("tanf_earned_income_deductions", period)
total_income = gross_earned + unearned
countable = total_income - deductions
return max_(countable, 0)
Why this is wrong:
- Deductions should ONLY reduce earned income
- Unearned income (SSI, child support, etc.) is not subject to work expense deductions
- This incorrectly reduces unearned income when earned income is low
Example error:
- Earned: $100, Unearned: $500, Deductions: $200
- Wrong result:
max_($100 + $500 - $200, 0) = $400 (reduces unearned!)
- Correct result:
max_($100 - $200, 0) + $500 = $500
✅ CORRECT - Apply deductions to earned only, then add unearned
def formula(spm_unit, period, parameters):
gross_earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
deductions = spm_unit("tanf_earned_income_deductions", period)
return max_(gross_earned - deductions, 0) + unearned
Pattern Variations
With multiple deduction steps:
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.xx.tanf.income
gross_earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
work_expense = min_(gross_earned * p.work_expense_rate, p.work_expense_max)
after_work_expense = max_(gross_earned - work_expense, 0)
earnings_disregard = after_work_expense * p.disregard_rate
countable_earned = max_(after_work_expense - earnings_disregard, 0)
return countable_earned + unearned
With disregard percentage (simplified):
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.xx.tanf.income
gross_earned = spm_unit("tanf_gross_earned_income", period)
unearned = spm_unit("tanf_gross_unearned_income", period)
countable_earned = gross_earned * (1 - p.earned_disregard_rate)
return max_(countable_earned, 0) + unearned
When Unearned Income HAS Deductions
Some states DO have unearned income deductions (rare). Handle separately:
def formula(spm_unit, period, parameters):
gross_earned = spm_unit("tanf_gross_earned_income", period)
gross_unearned = spm_unit("tanf_gross_unearned_income", period)
earned_deductions = spm_unit("tanf_earned_income_deductions", period)
unearned_deductions = spm_unit("tanf_unearned_income_deductions", period)
countable_earned = max_(gross_earned - earned_deductions, 0)
countable_unearned = max_(gross_unearned - unearned_deductions, 0)
return countable_earned + countable_unearned
Quick Reference
Standard TANF pattern:
Countable Income = max_(Earned - Earned Deductions, 0) + Unearned
NOT:
❌ max_(Earned + Unearned - Deductions, 0)
❌ max_(Earned - Deductions + Unearned, 0) # Can go negative
Federal/State Separation
Federal Parameters
Location: /parameters/gov/{agency}/
- Base formulas and methodologies
- National standards
- Required elements
State Parameters
Location: /parameters/gov/states/{state}/
- State-specific thresholds
- Implementation choices
- Scale factors
first_person: 14_580
fpg_multiplier: 2.0
State Income Tax Conformity to Federal Rules
CRITICAL: State income taxes should reference federal income sources and limits, not redefine them
Most state income taxes start with federal definitions and then make specific adjustments. When implementing state income tax:
✅ CORRECT - Reference federal income sources:
class ms_agi(Variable):
"""Mississippi adjusted gross income"""
value_type = float
entity = TaxUnit
definition_period = YEAR
label = "Mississippi adjusted gross income"
unit = USD
def formula(tax_unit, period, parameters):
federal_agi = tax_unit("adjusted_gross_income", period)
ms_additions = tax_unit("ms_additions_to_agi", period)
ms_subtractions = tax_unit("ms_subtractions_from_agi", period)
return federal_agi + ms_additions - ms_subtractions
❌ WRONG - Redefining income sources:
Why this matters:
- Federal income tax applies capital loss limits before reporting AGI
- State income taxes that start from federal AGI automatically inherit these limits
- Creating separate state income source parameters bypasses federal rules
- Results in incorrect calculations (e.g., unlimited capital loss deductions)
Common state conformity patterns:
- Full conformity - State AGI = Federal AGI (rare)
- Rolling conformity - State follows current federal rules
- Static conformity - State follows federal rules as of a specific date
- Selective conformity - State follows federal but with specific modifications
Implementation approach:
- Always start with federal income sources/AGI/taxable income as the base
- Use state parameters only for state-specific additions, subtractions, or modifications
- Reference federal variables:
adjusted_gross_income, taxable_income, etc.
- Don't recreate federal income aggregation logic at the state level
Example - Mississippi specifics:
class ms_additions_to_agi(Variable):
"""Mississippi additions to federal AGI"""
adds = [
"ms_state_bond_interest",
"ms_other_additions"
]
class ms_subtractions_from_agi(Variable):
"""Mississippi subtractions from federal AGI"""
adds = [
"ms_retirement_income_exclusion",
"ms_other_subtractions"
]
Code Reuse Patterns
Avoid Duplication - Create Intermediate Variables
❌ ANTI-PATTERN: Copy-pasting calculations
def formula(household, period, parameters):
gross = add(household, period, ["income"])
deduction = p.deduction * household.nb_persons()
return max_(gross - deduction, 0)
def formula(household, period, parameters):
gross = add(household, period, ["income"])
deduction = p.deduction * household.nb_persons()
after_deduction = max_(gross - deduction, 0)
return after_deduction < p.threshold
✅ CORRECT: Reuse existing variables
def formula(household, period, parameters):
countable_income = household("program_countable_income", period)
return countable_income < p.threshold
When to create intermediate variables:
- Same calculation in 2+ places
- Logic exceeds 5 lines
- Reference implementations have similar variable
Federal Aggregator Variables (Summing State Programs)
CRITICAL: Discover Programs by Enumerating State Directories
When building or modifying a federal-level variable that sums state programs (like tanf, which sums all state TANF programs), never search by keyword or naming pattern. State programs use wildly different names that don't match any single pattern.
Example — TANF program names across states:
- Standard:
al_tanf, ca_tanf, ny_tanf (28 states)
- Non-standard:
fl_tca, mn_mfip, ia_fip, ct_tfa, md_tca, mi_fip
- Completely unique:
ar_tea, id_tafi, la_fitap, ma_tafdc, ne_adc, oh_owf, ut_fep, wy_power, ky_ktap, nh_fanf, tn_ff
None of the "completely unique" names contain "tanf". A keyword search for "tanf" misses 11 programs.
Correct discovery method:
- List ALL state directories:
ls policyengine_us/variables/gov/states/
- For EACH of the 51 jurisdictions (50 states + DC), check non-tax subdirectories for the program type
- Look for the top-level benefit variable (entity=SPMUnit, has
defined_for)
- Add it to the aggregator list
Quick command to find all state TANF-like top-level variables:
grep -rl "entity = SPMUnit" policyengine_us/variables/gov/states/*/ \
--include="*.py" | \
xargs grep -l "defined_for" | \
Cycle Checks When Wiring Up State Programs
Adding a state program to a federal aggregator can create circular dependencies. Common cycles:
-
Housing cost cycle: State TANF → housing_cost → rent → housing_assistance → hud_annual_income → TANF
- Fix: Use
pre_subsidy_rent + other housing components instead of housing_cost
-
Childcare cycle: State TANF → childcare_expenses → childcare subsidies → SNAP → TANF
- Fix: Use
spm_unit_pre_subsidy_childcare_expenses instead of childcare_expenses
-
Entity broadcast bugs: Person-level and SPMUnit-level arrays mixed in where() — passes unit tests (scalar) but fails microsim (vectorized)
- Fix: Use
spm_unit.project(spm_unit_var) to broadcast to person level
After adding programs, always run the microsimulation test — it catches cycles and entity mismatches that unit tests miss.
TANF-Specific Patterns
Study Reference Implementations First
MANDATORY before implementing any TANF:
- DC TANF:
/variables/gov/states/dc/dhs/tanf/
- IL TANF:
/variables/gov/states/il/dhs/tanf/
- TX TANF:
/variables/gov/states/tx/hhs/tanf/
Learn from them:
- Variable organization
- Naming conventions
- Code reuse patterns
- When to use
adds vs formula
Standard TANF Structure
tanf/
├── eligibility/
│ ├── demographic_eligible.py
│ ├── income_eligible.py
│ └── eligible.py
├── income/
│ ├── earned/
│ ├── unearned/
│ └── countable_income.py
└── [state]_tanf.py
Simplified TANF Rules
For simplified implementations:
DON'T create state-specific versions of:
- Demographic eligibility (use federal)
- Immigration eligibility (use federal)
- Income sources (use federal baseline)
❌ DON'T CREATE:
ca_tanf_demographic_eligible_person.py
ca_tanf_gross_earned_income.py
parameters/.../income/sources/earned.yaml
✅ DO USE:
# Federal demographic eligibility
is_demographic_tanf_eligible
# Federal income aggregation
tanf_gross_earned_income
Avoiding Unnecessary Wrapper Variables (CRITICAL)
Golden Rule: Only create a state variable if you're adding state-specific logic to it!
Understand WHY Variables Exist, Not Just WHAT
When studying reference implementations:
- Note which variables they have
- READ THE CODE inside each variable
- Ask: "Does this variable have state-specific logic?"
- If it just returns federal baseline → DON'T copy it
Variable Creation Decision Tree
Before creating ANY state-specific variable, ask:
- Does federal baseline already calculate this?
- Does my state do it DIFFERENTLY than federal?
- Can I write the difference in 1+ lines of state-specific logic?
- Will this calculation be used in 2+ other variables? (Code reuse exception)
Decision:
- If YES/NO/NO/NO → DON'T create the variable, use federal directly
- If YES/YES/YES/NO → CREATE the variable with state logic
- If YES/NO/NO/YES → CREATE as intermediate variable for code reuse (see exception below)
EXCEPTION: Code Reuse Justifies Intermediate Variables
Even without state-specific logic, create a variable if the SAME calculation is used in multiple places.
❌ Bad - Duplicating calculation across variables:
class mo_tanf_income_eligible(Variable):
def formula(spm_unit, period, parameters):
gross = add(spm_unit, period, ["tanf_gross_earned_income", "tanf_gross_unearned_income"])
return gross <= p.income_limit
class mo_tanf_countable_income(Variable):
def formula(spm_unit, period, parameters):
gross = add(spm_unit, period, ["tanf_gross_earned_income", "tanf_gross_unearned_income"])
deductions = spm_unit("mo_tanf_deductions", period)
return max_(gross - deductions, 0)
class mo_tanf_need_standard(Variable):
def formula(spm_unit, period, parameters):
gross = add(spm_unit, period, ["tanf_gross_earned_income", "tanf_gross_unearned_income"])
return where(gross < p.threshold, p.high, p.low)
✅ Good - Extract into reusable intermediate variable:
class mo_tanf_gross_income(Variable):
adds = ["tanf_gross_earned_income", "tanf_gross_unearned_income"]
class mo_tanf_income_eligible(Variable):
def formula(spm_unit, period, parameters):
gross = spm_unit("mo_tanf_gross_income", period)
return gross <= p.income_limit
class mo_tanf_countable_income(Variable):
def formula(spm_unit, period, parameters):
gross = spm_unit("mo_tanf_gross_income", period)
deductions = spm_unit("mo_tanf_deductions", period)
return max_(gross - deductions, 0)
class mo_tanf_need_standard(Variable):
def formula(spm_unit, period, parameters):
gross = spm_unit("mo_tanf_gross_income", period)
return where(gross < p.threshold, p.high, p.low)
When to create intermediate variables for reuse:
- ✅ Same calculation appears in 2+ variables
- ✅ Represents a meaningful concept (e.g., "gross income", "net resources")
- ✅ Simplifies maintenance (change once vs many places)
- ✅ Follows DRY (Don't Repeat Yourself) principle
When NOT to create (still a wrapper):
- ❌ Only used in ONE place
- ❌ Just passes through another variable unchanged
- ❌ Adds indirection without code reuse benefit
Red Flags for Unnecessary Wrapper Variables
❌ INVALID - Pure wrapper, no state logic:
class in_tanf_assistance_unit_size(Variable):
def formula(spm_unit, period):
return spm_unit("spm_unit_size", period)
❌ INVALID - Aggregation without transformation:
class in_tanf_countable_unearned_income(Variable):
def formula(tax_unit, period):
return tax_unit.sum(person("tanf_gross_unearned_income", period))
❌ INVALID - Pass-through with no modification:
class in_tanf_gross_income(Variable):
def formula(entity, period):
return entity("tanf_gross_income", period)
Examples of VALID State Variables
✅ VALID - Has state-specific disregard:
class in_tanf_countable_earned_income(Variable):
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.in.tanf.income
earned = spm_unit("tanf_gross_earned_income", period)
return earned * (1 - p.earned_income_disregard_rate)
✅ VALID - Uses state-specific limits:
class in_tanf_income_eligible(Variable):
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.in.tanf
income = spm_unit("tanf_countable_income", period)
size = spm_unit("spm_unit_size", period.this_year)
limit = p.income_limit[min_(size, p.max_household_size)]
return income <= limit
✅ VALID - IL has different counting rules:
class il_tanf_assistance_unit_size(Variable):
adds = [
"il_tanf_payment_eligible_child",
"il_tanf_payment_eligible_parent",
]
State Variables to AVOID Creating
For TANF implementations:
❌ DON'T create these (use federal directly):
state_tanf_assistance_unit_size (unless different counting rules like IL)
state_tanf_countable_unearned_income (unless state has disregards)
state_tanf_gross_income (just use federal baseline)
- Any variable that's just
return entity("federal_variable", period)
✅ DO create these (when state has unique rules):
state_tanf_countable_earned_income (if unique disregard %)
state_tanf_income_eligible (state income limits)
state_tanf_maximum_benefit (state payment standards)
state_tanf (final benefit calculation)
Demographic Eligibility Pattern
Option 1: Use Federal (Simplified)
class ca_tanf_eligible(Variable):
def formula(spm_unit, period, parameters):
has_eligible = spm_unit.any(
spm_unit.members("is_demographic_tanf_eligible", period)
)
return has_eligible & income_eligible
Option 2: State-Specific (Different thresholds)
class ca_tanf_demographic_eligible_person(Variable):
def formula(person, period, parameters):
p = parameters(period).gov.states.ca.tanf
age = person("age", period.this_year)
age_limit = where(
person("is_full_time_student", period),
p.age_threshold.student,
p.age_threshold.minor_child
)
return age < age_limit
Common Implementation Patterns
Income Eligibility
class program_income_eligible(Variable):
value_type = bool
entity = SPMUnit
definition_period = MONTH
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.xx.program
income = spm_unit("program_countable_income", period)
size = spm_unit("spm_unit_size", period.this_year)
threshold = p.income_limit[min_(size, p.max_household_size)]
return income <= threshold
Benefit Calculation
class program_benefit(Variable):
value_type = float
entity = SPMUnit
definition_period = MONTH
unit = USD
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.xx.program
eligible = spm_unit("program_eligible", period)
base = p.benefit_schedule.base_amount
adjustment = p.benefit_schedule.adjustment_rate
size = spm_unit("spm_unit_size", period.this_year)
amount = base + (size - 1) * adjustment
return where(eligible, amount, 0)
Using Scale Parameters
def formula(entity, period, parameters):
p = parameters(period).gov.states.az.program
federal_p = parameters(period).gov.hhs.fpg
size = entity("household_size", period.this_year)
fpg = federal_p.first_person + federal_p.additional * (size - 1)
state_scale = p.income_limit_scale
income_limit = fpg * state_scale
Handling Parameter Structure Transitions
When a parameter changes structure over time (e.g., flat rate → marginal brackets), the parameter side uses a boolean toggle with separate files (see parameter patterns skill). The variable must branch on that toggle.
Pattern: Use if p.toggle: to select the right parameter access method:
class wa_capital_gains_tax(Variable):
value_type = float
entity = TaxUnit
definition_period = YEAR
unit = USD
defined_for = StateCode.WA
def formula(tax_unit, period, parameters):
p = parameters(period).gov.states.wa.tax.income.capital_gains
taxable_ltcg = ...
if p.rate.flat_applies:
return taxable_ltcg * p.rate.flat
return p.rate.incremental.calc(taxable_ltcg)
Why if (not where) is correct here:
p.rate.flat_applies is a parameter (scalar boolean at a given instant), not a per-entity variable
- Python
if is appropriate because the entire population uses the same rate structure in a given year
where() is for per-entity branching (e.g., different treatment by filing status)
When to use this pattern:
- ✅ A flat rate became a marginal bracket schedule at a specific date
- ✅ A single value became a lookup table at a specific date
- ✅ Any parameter whose access method (
.calc() vs *) changes by period
When NOT to use this pattern:
- ❌ The parameter structure is the same across all periods (just access it normally)
- ❌ The branching depends on a per-entity condition like income or age (use
where() instead)
- ❌ A new bracket was added to an existing scale using
.inf (no variable changes needed — .calc() works as before; see parameter patterns skill)
Gating Provisions with in_effect Boolean
When a provision starts or ends at a specific date, use if p.provision.in_effect: to gate the entire logic block. The parameter side has an in_effect.yaml boolean (see parameter patterns skill). The variable wraps the provision's logic in a plain if block.
Real-world example — CT TFA high-earnings reduction (new in 2024):
class ct_tfa(Variable):
value_type = float
entity = SPMUnit
definition_period = MONTH
unit = USD
defined_for = "ct_tfa_eligible"
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.ct.dss.tfa.payment
payment_standard = spm_unit("ct_tfa_payment_standard", period)
countable_unearned_income = spm_unit(
"ct_tfa_countable_unearned_income", period
)
raw_benefit = max_(payment_standard - countable_unearned_income, 0)
if p.high_earnings.in_effect:
gross_earnings = add(
spm_unit, period, ["tanf_gross_earned_income"]
)
fpg = spm_unit("tanf_fpg", period)
high_income_threshold = p.high_earnings.rate * fpg
high_income = gross_earnings >= high_income_threshold
reduction_multiplier = 1 - p.high_earnings.reduction_rate
return where(
high_income,
raw_benefit * reduction_multiplier,
raw_benefit,
)
return raw_benefit
Key points:
if p.high_earnings.in_effect: uses Python if because it's a parameter (scalar boolean), not a per-entity variable
- The
if block is NEVER entered for periods before the effective date (2024)
- Inside the
if block, where() IS used for per-entity branching (some households have high income, others don't)
- The provision's sub-parameters (
rate, reduction_rate) are only accessed inside the guarded block
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 with its own sub-parameters
When NOT to use this pattern:
- ❌ The branching depends on a per-entity condition (use
where() instead)
- ❌ The parameter structure changes (use
flat_applies pattern above)
Regional Variation with regional_in_effect Boolean
When a program transitions between regional and statewide payment standards, use if p.regional_in_effect: to switch between regional enum lookup and flat parameter access.
Real-world example — CT TFA payment standard (regional before 2022, flat after):
class ct_tfa_payment_standard(Variable):
value_type = float
entity = SPMUnit
definition_period = MONTH
unit = USD
defined_for = StateCode.CT
def formula(spm_unit, period, parameters):
p = parameters(period).gov.states.ct.dss.tfa.payment
size = spm_unit("spm_unit_size", period.this_year)
capped_size = min_(size, p.max_unit_size)
if p.regional_in_effect:
region = spm_unit.household("ct_tfa_region", period)
region_a = region == region.possible_values.REGION_A
region_c = region == region.possible_values.REGION_C
return select(
[region_a, region_c],
[
p.regional.region_a.amount[capped_size],
p.regional.region_c.amount[capped_size],
],
default=p.regional.region_b.amount[capped_size],
)
return p.amount[capped_size]
Key points:
if p.regional_in_effect: uses Python if — scalar parameter boolean
- Inside the
if block, select() handles per-entity branching by region enum
- The
default parameter in select() handles regions not explicitly listed
- When
regional_in_effect is false, falls through to the flat p.amount[capped_size]
- Region enum variable (
ct_tfa_region) is only accessed when regional variation is active
When to use this pattern:
- ✅ Program transitions from regional to statewide standards (or vice versa)
- ✅ Regional lookup uses an enum variable with
select()
- ✅ The flat and regional parameter trees have different structures
When NOT to use this pattern:
- ❌ Regional variation always applies (just use regional parameters directly)
- ❌ The variation is by household characteristic, not geography (use
where())
Choosing Between Boolean Toggle Patterns (Summary)
All three patterns use if p.boolean: branching on a scalar parameter. Here's when to use each:
| Pattern | Parameter Side | Variable Side | Use Case |
|---|
flat_applies | Folder with flat + bracket + toggle | if p.flat_applies: switches access method | Structure changes (flat → brackets) |
in_effect | Single in_effect.yaml + sibling params | if p.provision.in_effect: gates logic block | Provision starts/ends at date |
regional_in_effect | Single boolean + regional/ folder + flat | if p.regional_in_effect: switches lookup | Regional ↔ statewide transition |
Common rule: All use Python if (not where) because parameters are scalar booleans at a given instant — the entire population uses the same code path for a given period.
Accessing Baseline Parameters in Reform Simulations
When You Need Baseline vs Reform Comparison
Some variables need to compare values under current law (baseline) vs a proposed reform. This is common for:
- Variables calculating the change in a value due to a reform
- Fixed-cost employer variables (e.g.,
employer_NI_fixed_employer_cost_change)
- Any variable showing "difference from baseline"
The Pattern for Accessing Baseline Parameters
CRITICAL: When a simulation has a baseline (i.e., it's a reform simulation), you must explicitly access baseline parameters:
def formula(person, period, parameters):
simulation = person.simulation
if simulation.baseline is not None:
baseline_parameters = simulation.baseline.tax_benefit_system.get_parameters_at_instant(period)
baseline_value = baseline_parameters.gov.hmrc.national_insurance.some_rate
else:
baseline_parameters = parameters(period)
baseline_value = baseline_parameters.gov.hmrc.national_insurance.some_rate
reform_value = parameters(period).gov.hmrc.national_insurance.some_rate
return reform_value - baseline_value
Common Mistake
❌ WRONG - Using current parameters for baseline:
def formula(person, period, parameters):
p = parameters(period)
baseline_rate = p.gov.hmrc.national_insurance.some_rate
reform_rate = p.gov.hmrc.national_insurance.some_rate
return reform_rate - baseline_rate
✅ CORRECT - Properly accessing baseline:
def formula(person, period, parameters):
simulation = person.simulation
if simulation.baseline is not None:
baseline_p = simulation.baseline.tax_benefit_system.get_parameters_at_instant(period)
else:
baseline_p = parameters(period)
baseline_rate = baseline_p.gov.hmrc.national_insurance.some_rate
reform_rate = parameters(period).gov.hmrc.national_insurance.some_rate
return reform_rate - baseline_rate
When This Matters
This pattern is essential when:
- The variable name contains "change", "difference", or "delta"
- The variable compares policy scenarios
- You're implementing reform impact analysis variables
Without this pattern, reform simulations will incorrectly show zero change because both "baseline" and "reform" values come from the same (reform) parameters.
Variable Creation Checklist
Before creating any variable:
Quality Standards
Complete Implementation Requirements
- All values from parameters (no hard-coding)
- Complete formula logic
- Proper entity aggregation
- Correct period handling
- Meaningful variable names
- Proper metadata
Anti-Patterns to Avoid
- Copy-pasting logic between files
- Hard-coding any numeric values
- Creating duplicate income variables
- State-specific versions of federal rules
- Placeholder TODOs in production code
Parameter-to-Variable Mapping Requirements
Every Parameter Must Have a Variable
CRITICAL: Complete implementation means every parameter is used!
When you create parameters, you MUST create corresponding variables:
| Parameter Type | Required Variable(s) |
|---|
| resources/limit | state_program_resources_eligible |
| income/limit | state_program_income_eligible |
| payment_standard | state_program_maximum_benefit |
| income/disregard | state_program_countable_earned_income |
| categorical/requirements | state_program_categorically_eligible |
Complete Eligibility Formula
The main eligibility variable MUST combine ALL checks:
class state_program_eligible(Variable):
def formula(spm_unit, period, parameters):
income_eligible = spm_unit("state_program_income_eligible", period)
resources_eligible = spm_unit("state_program_resources_eligible", period)
categorical = spm_unit("state_program_categorically_eligible", period)
return income_eligible & resources_eligible & categorical
Common Implementation Failures:
- ❌ Created resource limit parameter but no resource_eligible variable
- ❌ Main eligible variable only checks income, ignores resources
- ❌ Parameters created but never referenced in any formula
For Agents
When implementing variables:
- Study reference implementations (DC, IL, TX TANF)
- Never hard-code values - use parameters
- Map every parameter to a variable - no orphaned parameters
- Complete ALL eligibility checks - income AND resources AND categorical
- Reuse existing variables - avoid duplication
- Use
adds when possible - cleaner than formula
- Create intermediate variables for complex logic
- Follow metadata standards exactly
- Complete implementation or delete the file