For full lists of effects, triggers, modifiers, and dynamic variables, see the corresponding files in resources/documentation/.
Stored on a scope (country, state, unit leader). Survive saves.
SOV = { set_variable = { var = my_var value = 5 } }
SOV.my_var # read from another scope
Exist only for the current scripted block. Prefixed with temp_ in effects but accessed by name.
set_temp_variable = { var = my_temp value = 10 }
Stored globally, not on any scope.
set_global_variable = { var = global_my_var value = 1 }
global.global_my_var # read globally
Accessed via ^ subscript (zero-indexed). Arrays cap at 1000 elements — add_to_array beyond index 999 silently does nothing.
my_array^0 # first element
my_array^3 # fourth element
my_array^i # element at index i (dynamic index from a loop variable)
my_var # local variable on current scope
ROOT.my_var # variable on ROOT scope
GER.my_var # variable on specific country
my_array^0 # array element by literal index
my_array^i # array element by dynamic loop index
var:my_var = { ... } # scope into the country/state stored in my_var
var:my_array^i = { ... } # scope into the country stored at array[i] ← CORRECT
var:v = { ... } # scope into the country stored in loop value v ← CORRECT
var:v^i = { ... } # WRONG — v is a scalar value, not an array
Key rule: value = v in a loop stores the scalar element (e.g. a country tag number) into v. It is NOT an array reference. To scope into the country at position i, use either var:v = { ... } (v holds the country reference) or var:ARRAYNAME^i = { ... } (explicit array name + dynamic index). Never var:v^i — v is a scalar and ^i does not apply.
All use { var = X value = Y } syntax. All have _temp_ equivalents (e.g. add_to_temp_variable).
Variables: set_variable, add_to_variable, subtract_from_variable, multiply_variable, divide_variable, modulo_variable, round_variable, clamp_variable (min/max), set_variable_to_random
Arrays: add_to_array, remove_from_array (by value or index), clear_array, resize_array, find_highest_in_array, find_lowest_in_array, random_scope_in_array
Short forms: add_to_array = { my_array = 42 }, remove_from_array = { my_array = 42 }, is_in_array = { my_array = 42 }
Preferred over scratch temp variables for pure arithmetic. An inline calculator that reads constants, scoped variables, and dynamic game variables, then applies a sequence of operations. Supported as the value argument of every variable/temp-variable effect except modulo_variable and clamp_variable.
Default to a math expression instead of a chain of set_temp_variable + add_to_variable + multiply_variable whenever the goal is a calculated value. One expression replaces several temp-var writes, and the engine evaluates it in a single pass — it is the more performance-friendly form, not just the shorter one. The win compounds with frequency: the hotter the path (per-tick GUI, daily on_action, AI evaluation), the more a math expression saves over scratch temp variables. Reach for temp variables only when you need to reuse an intermediate result across several later effects, or when an operation the expression syntax doesn't support (modulo_variable, clamp_variable) is involved.
The math expression is the value of the effect. Wrap it in { ... } so the parser can tell where the expression ends and the effect's own keywords begin. The expression is a base value = ... followed by sequential statements that mutate an accumulator. Each statement is one of:
- No-argument — applies an operation to the accumulator:
round = yes. - Simple — takes one or more sub-expressions:
add = 5,clamp = { min = 0 max = 100 }. - Control flow — takes a block:
if = { limit = { ... } add = 100 } else = { subtract = 1 }. - Collection iterator — scopes to each element of a named collection and applies statements:
every_collection = { ... }.
The full statement list, with per-statement examples, is in resources/documentation/script_math_functions.md.
set_variable / set_temp_variable, two equivalent shapes:
# Long form: var name as the key, math expression as its value
set_temp_variable = {
foo = {
value = bar
multiply = 2
add = baz
}
}
# Short form: var = X plus value = { ... }
set_temp_variable = {
var = foo
value = {
value = bar
multiply = 2
add = baz
}
}
Both compile to the same result. The long form is one block shallower (no var = line). The short form reads more like the accumulate shape below and is the one to reach for when a tool or surrounding context expects var = to be explicit.
Do not write the math expression as siblings of var = X. That form silently parses to 0.0 (no error, no log) and is the most common cause of "my set_temp_variable is mysteriously producing 0" reports:
# WRONG, silently evaluates to 0.0
set_variable = {
var = combined_units
value = num_cavalry
add = num_motorized
add = num_mechanized
}
The fix is to wrap the expression inside value = { ... } (short form) or use var_name = { ... } (long form). Every math expression in the mod follows one of these two shapes; copy from 00_money_system.txt, 99_eu_scripted_effects.txt, 00_productivity_effects.txt, !_energy_effects.txt, 01_missiles_scripted_localisation.txt, or 01_money_scripted_localisation.txt if in doubt.
The expression is also the value of an accumulate effect (add_to_variable,
subtract_from_variable, multiply_variable, divide_variable), not just
set_variable. This is rare in the codebase (vanilla and MD historically used a
scratch temp instead) but confirmed working in-engine. Prefer it to fold a
single-use temp straight into the accumulate:
# Instead of: set_temp_variable = { tmp = { ...expr... } } then add_to_variable = { X = tmp }
add_to_variable = {
var = global.cumulative_world_productivity
value = {
value = overall_productivity
multiply = 0.001
multiply = population_total_m
}
}
For a self-referencing accumulate (X = X + expr) the equivalent
set_variable = { X = { value = X add = { ...expr... } } } also works and reads
the pre-write value, since the full RHS evaluates before assignment.
- Fixed-point arithmetic throughout (same as HOI4 variables).
- Booleans:
0.0is false, any other value is true. Comparison/boolean operators return1.0(true) or0.0(false). - Parse failure →
0.0. A malformed expression evaluates to zero. It is silent in-game (no tooltip, no crash, the calculation just reads 0), but it does log at load:script_math.cpp:350: Errors occurred while reading math expression defaulting to 0. Watch for this when a calculation mysteriously reads 0. - One bad expression cascades through the rest of the file. The parser mis-consumes a brace and reads everything after it one level off, so later
set_variableblocks in unrelated effects report bogus errors (invalid left side variable: {,Math program must start with value =: <token>). Always fix the firstscript_matherror in a file and re-run before chasing the others. Brace counts still balance, so a text-level check will not catch it.
| Statement | Effect |
|---|---|
add, subtract, multiply, divide |
Arithmetic on the accumulator |
min, max |
Accumulator = min/max of itself and value |
clamp = { min = X max = Y } |
Bound accumulator (argument order matters) |
greater_than, less_than |
Return 1.0/0.0 |
round = yes |
Round to nearest integer |
if = { limit = { ... } ... } else = { ... } |
limit is an expression; true if non-zero |
every_collection = { named_collection = X ... } |
Run statements per element of a collection |
Each operator's argument is itself a full expression, so they nest:
greater_than = { value = num_units multiply = 0.4 } # accumulator > (num_units * 0.4)
Only greater_than and less_than are safe comparators. equals, not_equals, greater_than_or_equals, and less_than_or_equals appear in Paradox-adjacent references, but the mod's one use of equals inside an if's limit (00_ct_effects.txt, counter-terror ambition_chance) threw a load-time script_math error and zeroed the expression, killing every terror-org attack roll. Vanilla never uses anything but less_than here (10 uses, zero equals), and MD has 29 greater_than / 7 less_than. validate_math_expressions.py reports the sibling-operator, unsafe-comparator and FROM-read traps (all three classes on this page) as warnings.
For an equality test, either rewrite as a strict inequality on an integer variable (equals = 0 on a 0/1/2 value becomes less_than = 1), or hoist the branch out of the expression entirely and use a normal effect-level if with check_variable:
# Inside the expression: works, but only with < and >
if = { limit = { value = global.active_terror_org_reach^i less_than = 1 } add = 15 }
# Hoisted out: unambiguous, and what to reach for when in doubt
set_temp_variable = { ambition_chance = { value = v subtract = { ... } } }
if = {
limit = { check_variable = { global.active_terror_org_reach^i = 0 } }
add_to_temp_variable = { ambition_chance = 15 }
}
set_temp_variable = { ambition_chance = { value = ambition_chance multiply = 0.2 round = yes } }
Re-assigning a variable from itself (value = ambition_chance above) is the standard way to continue an expression after a hoisted branch.
The engine accepts a construct or silently zeroes it, so grep for precedent before using anything unfamiliar in a math expression:
grep -rn "your_construct" common/ | head # does MD already ship it?
grep -rn "your_construct" "$HOI4/common/" | head # does vanilla?No hits in either means no evidence it parses. Confirmed working in MD: ^num as an operand (01_BRICS_effects.txt:104), nested operand blocks (!_energy_effects.txt:254, 00_influence_scripted_effects.txt:258), round = yes (bankruptcy_decisions.txt:139), clamp = { min max } (00_scripted_triggers.txt:494), dynamic array indices (array^i), and if inside an expression (vanilla factions/goals/faction_goals_short_term.txt:266).
Known broken: reading a variable through the event/on_action FROM binding (value = FROM.debt_bailout) inside a math expression. It parses cleanly (no script_math error) but reads 0 at runtime, zeroing the whole expression (#2464, bailout donors paid $0 but still gained influence); neither vanilla nor MD has a verified working use. ROOT./THIS./PREV. reads inside expressions do have working precedent (00_money_system.txt:920, 99_eu_scripted_effects.txt:1441, 00_productivity_effects.txt:33). Keep the math expression and hoist the FROM read into a plain temp copy it can reference: set_temp_variable = { bailout_cost = FROM.debt_bailout } then set_temp_variable = { treasury_change = { value = bailout_cost multiply = -0.75 } }.
for_each_loop = {
array = my_array
value = v # current element value (default 'v')
index = i # current index (default 'i')
break = brk # set this var to non-zero to break (default 'break')
# effects...
}
v = the scalar value at position i. To scope into a country stored at i:
var:my_array^i = { ... } # recommended — uses array name
var:v = { ... } # also valid — v holds the country reference
for_each_scope_loop = {
array = my_array
break = brk # optional
tooltip = loc_key # optional tooltip
# effects run inside each element's scope automatically
}
for_loop_effect = {
start = 0 # default 0
end = 10
compare = less_than # default less_than; also: less_than_or_equals, greater_than, etc.
add = 1 # step (default 1)
value = v # loop counter variable (default 'v')
break = brk # optional break variable
# effects...
}
while_loop_effect = {
limit = { check_variable = { counter < target } }
# body — must advance the condition or loop exits at 1000
}
Engine hard-caps at 1000 iterations (not configurable). max_iterations is not a valid key and is silently ignored. Design loops so the realistic worst case stays well below 1000.
any_of = {
array = my_array
value = v # current element scalar (default 'value')
index = i # current index (default 'index')
# triggers — returns true if ALL triggers true for at least one element
}
Returns false if array is empty or no element satisfies all triggers.
all_of — same syntax as any_of, returns true only if ALL elements match.
any_of_scopes / all_of_scopes — auto-scope into each element (no value/index variables):
any_of_scopes = {
array = my_array
# triggers evaluated inside each element's scope
}
any_of vs any_of_scopes: any_of stays in current scope (access via var:v); any_of_scopes auto-scopes into each element (simpler for country/state arrays).
check_variable = { my_var > 12 } # shorthand (also =, <)
check_variable = { var = my_var value = 12 compare = greater_than } # explicit
Compare values: less_than, less_than_or_equals, greater_than, greater_than_or_equals, equals, not_equals.
is_in_array = { my_array = 42 }— check membershipvar:my_var = { exists = yes }— check if country in variable actually exists in-game
Computed by the engine. Full list in resources/documentation/dynamic_variables_documentation.md or the Paradox Wiki — Game variables.
Common: global.countries, global.majors, global.states, global.year, global.threat, num_of_civilian_factories, num_of_military_factories, stability, war_support, political_power, manpower, faction_members, allies, subjects.
Engine-provided scope arrays. Usable anywhere an array name is accepted: target_array = X on decisions, array = X inside for_each_scope_loop / every_country / any_of_scopes, and inside collection operators. Full list on the Paradox Wiki — Game arrays.
| Array | Contents |
|---|---|
global.countries |
Every country in the game, including non-existing dynamic tags |
global.majors |
Every country currently marked major |
global.states |
Every state in the game |
global.ideology_groups |
Every ideology group |
global.operations |
Every operation |
global.technology |
Every technology |
global.province_controllers |
Controller by province ID: global.province_controllers^1234 |
| Array | Contents |
|---|---|
allies |
Fellow faction members + subjects + overlord |
faction_members |
All members of the current country's faction |
subjects |
Current country's subjects |
occupied_countries |
Countries currently occupied by this one |
enemies |
Countries currently at war with the current country |
potential_and_current_enemies |
Current enemies + allies-of-enemies + countries with wargoals |
enemies_of_allies |
Enemies of any of the current country's allies |
neighbors |
Countries sharing a border via controlled provinces |
neighbors_owned |
Countries sharing a border via owned states |
owned_states |
States owned (but not necessarily controlled) |
controlled_states |
States controlled (but not necessarily owned) |
owned_controlled_states |
States both owned and controlled |
core_states |
States considered national territory |
army_leaders, navy_leaders |
Recruited characters |
operatives |
Recruited operatives |
researched_techs |
Technologies already researched |
exiles |
Exiled governments this country is hosting |
| Array | Contents |
|---|---|
core_countries |
Countries that consider this state national territory |
For target_array on decisions — the canonical way to scope a targeted decision to a narrow, engine-maintained set of countries. Prefer these over target_array = global.countries + a filter trigger, which iterates every country in the game daily.
# Good — iterates only current subjects, target_trigger filters within them
target_array = subjects
target_trigger = {
FROM = { influence_higher_5 = yes }
}
# Good — iterates only land-bordering neighbors
target_array = neighbors
target_trigger = {
FROM = { check_variable = { FROM.gdp_total > ROOT.gdp_total } }
}
# Worse — iterates every country in the game, relies on trigger to filter
target_array = global.countries
target_trigger = {
FROM = { is_neighbor_of = ROOT }
}
For any_of_scopes / every_country / for_each_scope_loop — pass the array name via array = X.
any_of_scopes = {
array = faction_members
NOT = { has_war = yes }
}
Sets of game objects supporting chained operators for filtering and expansion — more efficient than manual array loops for many use cases.
collection_size = {
input = {
input = game:scope # base input
operators = { faction_members owned_states } # chained operators
name = "States owned by faction" # optional display name
}
value > 42
}
| Input | Description |
|---|---|
game:all_countries |
All existing countries (including government in exile) |
game:all_possible_countries |
All countries (including non-existing) |
game:all_states |
All existing states |
game:scope |
Current scope object |
collection:NAME |
Named collection |
constant:NAME |
Script constant |
| Operator | Description |
|---|---|
faction_members |
All faction members of the country (including itself) |
owned_states |
All states owned by the country |
controlled_states |
All states controlled by the country |
country_and_all_subjects |
The country and all its subjects |
trigger = { ... } |
Filter by trigger (used inside limit) |
my_collection = game:all_states # equivalent to { input = game:all_states }
For full collection docs, see resources/documentation/script_collection_input.md and script_collection_operator.md.
Reusable constants usable across all script files (unlike file-local @ macros). No runtime cost. Usage: constant:numeric_constants.pi. See resources/documentation/script_concept_documentation.md.
Used in custom_effect_tooltip and other bindable-loc contexts. Three forms:
# Simple loc key
custom_effect_tooltip = MY_TOOLTIP
# Formatter (generates text from game data — e.g., idea description)
custom_effect_tooltip = idea_desc|canadian_pacific_railway
# Bound localization (parameter injection)
custom_effect_tooltip = {
localization_key = MY_TOOLTIP
PARAM_NAME = OTHER_LOC_KEY
}
Available formatters: idea_desc, idea_name, tech_effect, advisor_desc, country_leader_desc, character_name, country_culture, building_state_modifier. See resources/documentation/loc_formatter_documentation.md for parameters and scope requirements.
Access scope objects in loc strings using [Object.Property] syntax:
"[Root.GetName] has signed a treaty with [FROM.GetName]"
"[Root.Capital.GetName] is under threat"
"[This.GetLeader] addresses the nation"
Object promotions (scope changes): Owner, Capital, OriginalCapital, Overlord, FactionLeader, Controller, Occupied.
Common properties: GetName, GetTag, GetFlag, GetAdjective, GetLeader, GetFactionName.
For full object/property lists, see resources/documentation/loc_objects_documentation.md.