Rules and Conditions
A policy's rules run top to bottom. For each rule: skip it if enabled
is false, skip it if any condition fails, otherwise apply its transform
to the running price and move on. Conditions are ANDed - one miss skips
the rule.
Rule types
rule_type | Params | Effect on the running price |
|---|---|---|
multiply | multiplier (0.01-100) or multiplier_field | Multiply by a literal or a context value |
add | amount (any number, negative allowed) | Add a flat amount |
subtract_percent | percentage (0-100) | Apply a percentage discount |
set_fixed | fixed_price (>= 0) | Replace the price outright |
set_from_lookup | lookup_table, key_field | Replace the price from a table, keyed by context |
round_psychological | charm_ending (0-0.999, default 0.99) | Charm-price the current value |
Because rules chain, order matters: multiply then add differs from
add then multiply, and a round_psychological placed last cleans up
whatever the earlier rules produced. set_fixed and set_from_lookup
discard the incoming price entirely - verified: a set_from_lookup rule
turned a 5.00 base price into the table's 18.00 before a later surge
multiplied it.
round_psychological floors to the charm ending (21.6 became 21.99 - it
can round up), drops whole numbers first (20.00 becomes 19.99), and
leaves prices under 1.00 untouched.
Conditions
Each condition is { "field": ..., "operator": ..., "value": ... }.
field is a dot path into the calculation context (user.tier,
zone.demand). A field that is absent from the context fails the whole
calculation with 400 field_not_found - it does not quietly skip the
rule.
| Operator | value shape | Meaning |
|---|---|---|
equals / not_equals | any JSON | Exact JSON equality |
greater_than / greater_than_or_equal | number | Numeric compare |
less_than / less_than_or_equal | number | Numeric compare |
between | [min, max] | Inclusive range, numeric or string |
in / not_in | array | Membership |
time_in_range | [start, end] | Clock window, wraps midnight |
time_in_range accepts "HH:MM" strings or numeric hours on both the
field and the range, and handles wrap-around: with ["22:00", "05:00"], a
context clock of 23:30 matches, 04:00 matches, and 12:00 does not.
{
"name": "vip_discount",
"rule_type": "subtract_percent",
"params": { "percentage": 20.0 },
"conditions": [
{ "field": "user.tier", "operator": "in", "value": ["vip", "gold"] }
]
}Lookup tables
set_from_lookup resolves a key from the context (key_field) against
a named table and sets the price to the mapped number. Tables are flat
maps of string keys to numbers, created separately (platform admin or
editor role):
snug surge-pricing lookup-table create --name zone-base \
--json '{"downtown": 12.5, "airport": 18.0, "suburbs": 9.0}'
snug surge-pricing lookup-table get --name zone-base{ "table_name": "zone-base", "entries": 3 }The key value may be a string, number, or boolean in the context; numbers and booleans are matched against their string form in the table.
References resolve at calculate time, not create time - all reproduced live:
- A policy naming a nonexistent table creates fine; calculating with it
fails
404 lookup_table_not_found. - A context key with no table entry fails
400 field_not_foundwith the message namingtable.key(zone-base.harbor). - A table entry that is not a number fails
400 invalid_field_value.
There is no list or delete endpoint for lookup tables - create with
the same name replaces the table.
Debugging a rule pipeline
--explain on a calculation returns one step per rule that actually
ran, in order, with the price after each - skipped rules (disabled or
conditions unmet) simply do not appear. The same breakdown comes back
from trace get later and from every simulate result. See the
README for captured output.