Rule Structure
${{ }} wrapper marks a value as a CEL expression (see Rule Actions: Dynamic action fields).
Conditions
Conditions are written in CEL (Common Expression Language), a simple expression language for evaluating boolean conditions against event data and participant state.Rule sets and evaluation order
Every rule belongs to a rule set. Every program starts with an immutable-keydefault set, and rule writes that omit rule_set_id go there.
The engine evaluates all applicable ACTIVE rules by rule-set order, then by rule order within each set. A rule with stop_after_match: true that matches and executes stops the remaining rules in its own set only; other sets continue, and a budget skip does not trigger the stop.
To place a rule when creating or moving it, use position (first, last, or before/after with a reference_id); omit it to append. Scrip numbers positions 1..N with no gaps.
Rule sets covers execution order, set-local stopping, and how to structure sets. See Managing rule configuration for consistent reads, version-guarded moves, atomic change sets, history, and rollback.
Time-Windowed Rules
Useactive_from and active_to to schedule rules for specific periods. Outside the window, the rule is skipped during evaluation.
Time windows are checked against the event’s event_timestamp, the same value CEL sees as now, not the wall-clock time at processing. active_from is inclusive and active_to is exclusive. A matching rule outside its window is recorded as a skipped evaluation with reason OUTSIDE_TIME_WINDOW.
- Delayed processing, retries, and replays make the same decision because they read the same timestamp.
- Event timestamps are supplied by the caller, so a backdated event (a historical import, for example) can land inside a window that has already closed on the calendar and still fire the rule.
event_timestamp fire both rules and earn 2x total. Events stamped January 1st or later skip the promo rule (active_to is exclusive) and the base rule continues on its own.
Budgets
Budgets cap how much a rule can issue per asset over a given period. When the budget is exhausted, the rule still matches but all of its actions are skipped for that evaluation. You might use a budget to limit a referral rule to $10,000/month or cap a promotional rule at 50,000 points total. A budget is a single shared pool, not a per-participant allowance. Thelimit applies to total issuance for the rule across every participant in the program. A 10000 point budget means the rule issues 10,000 points combined to all participants, not 10,000 per participant. Consumption is tracked per (rule, asset), so each entry in the budgets array is one program-wide cap.
To cap how much an individual participant can earn, don’t use a budget. Instead, track the participant’s earnings in a counter and gate the rule on it. Add a COUNTER action that increments a per-participant counter by the credited amount, then guard the rule with a condition that checks the counter against the cap:
referral_points counter reaches 500, regardless of how many other participants the rule has paid out. A budget and a counter cap are independent: combine both to cap per-participant earning and total program spend at the same time.
Budgets are defined inline on the rule as an array of per-asset limits:
Schedule Types
A lifetime cap omitsschedule_type:
CRON resets on a calendar-aligned schedule, tied to the same wall-clock times regardless of when the rule was created:
INTERVAL resets after a fixed duration, timed from budget creation and restarting after each reset:
Consumption
Each time the rule fires a credit action, the amount is checked against the budget and added to a singleconsumed total shared by all participants. If the consumed amount plus the new amount would exceed the limit, all of the rule’s actions are rolled back and the evaluation is recorded as skipped with reason BUDGET_EXCEEDED. Consumption is atomic, so concurrent events can’t overspend, even when different participants trigger the rule at the same time.
The response for any rule includes the current budget state:
Resetting a Budget
Scheduled budgets reset automatically whennext_reset_at arrives. You can also reset a budget manually:
consumed back to zero and advances next_reset_at to the next scheduled reset. For lifetime budgets, the consumed amount resets but no future reset is scheduled.
Updating Budgets
Budgets are updated as part of the rule. Include the fullbudgets array in your update request and it replaces the previous one. Omitting the field leaves budgets unchanged.
State Snapshot Evaluation Behavior
When an event is processed, Scrip snapshots participant state, program state, and group state at the start. All rule conditions and dynamic action expressions within that event evaluate against this snapshot, not the live database. That means if Rule A increments a counter, adds a tag, sets an attribute, or assigns a tier, Rule B still sees the original value even though it evaluates after Rule A. This applies to counters, tags, attributes, tiers, program state, and group state. It also applies to dynamic action expressions:CREDIT / DEBIT / HOLD / RELEASE / FORFEIT amounts, COUNTER values, SET_ATTRIBUTE values, and reference_id expressions all use the same event-start snapshot. The snapshot is shared across every rule set. State actions write durable updates sequentially in set and rule order, but those updates become inputs for future events, not later conditions or action expressions in the same event.
Example
spend is 900 and event.amount is 200:
- Rule 1 executes, updating the counter to 1100 in the database
- Rule 2 evaluates against the snapshot value (900), so it does not match
TAG action that adds vip, a Rule B condition that checks "vip" in participant.tags still evaluates against the pre-event tag set. The tag is visible on the next event.
Threshold Crossing Pattern
To detect when a counter crosses a threshold during an event, check the pre-event value plus the event amount:SCHEDULE_EVENT).
See CEL Expressions for more patterns including milestones, date ranges, and capped bonuses.
Rule Status
Updating rules under live traffic
Scrip reads a program’s rule definitions at processing time: the moment a worker picks up the event, not when you submit it. Each queued event is evaluated against whatever the rules say at the instant it is processed. This matters when you change a rule (its condition, actions, amount, containing set, order, or status) while events are still in flight:- Events already processed keep the result from the previous definition.
- Events not yet processed use the new definition.
- A batch that spans your change can split across both versions, because workers process queued events independently and at slightly different times.
rule_set_id and a rule_history_id that links to the exact rule version active when the event was evaluated:
GET /v1/programs/{programId}/rule-configuration returns one consistent snapshot and its rule_configuration_version. Version-guarded move, reorder, preview, and rollback requests reject stale plans with 409 rule_configuration_version_conflict. See Managing rule configuration.
If you need every event in a batch to use a single definition, stop submitting affected events and let the queue drain before you make the change.
Validation and Simulation
Save-time validation
Rule create and update validate every action expression, not only the condition. Template syntax errors, CEL compile errors, and references to unknown participant fields, tier keys, or asset symbols are rejected with a400 that names the field path (for example, actions[0].amount). A static amount that is zero or negative is also rejected.
The condition field is always CEL and never uses the ${{ }} wrapper. A condition containing ${{ is rejected at save with condition is always a CEL expression; write it without the ${{ }} wrapper, so the GitHub Actions habit of wrapping expressions fails loudly instead of as a cryptic parse error. The same check applies to automation participant_filter and guard_condition.
Create, update, and validate responses include a non-blocking warnings array. Warnings cover both the condition and action expressions, and include deprecated_alias when an expression uses the deprecated state variable: CEL variable "state" is deprecated; use "participant" instead. Rules with warnings still save and evaluate.
Validate a condition
Check a condition, and optionally draft actions, before creating a rule:program_id to check tier keys and asset symbols against that program’s configuration and receive vocabulary warnings, and an optional actions array to run the same save-time checks on draft actions. event.* field references are not verified because events are schemaless: a misspelled event field saves fine and then silently never matches. To see which event fields a program’s rules currently depend on, call the event references endpoint. To see which fields appear in recent traffic, call List observed event shapes.
Simulate a rule
Test a rule against sample data without persisting any changes:participant_state field is optional. The response includes whether the condition matched and, for each action, the evaluated result (resolved amounts, projected counter values, etc.).
To simulate against a real participant’s current state instead of mocking it, pass participant_id (mutually exclusive with participant_state):
missing_participant_context warning and the condition evaluates against empty defaults.
Counter values in participant_state may be JSON numbers or decimal strings (the format the participant state endpoints return), so you can paste a counters map from a real state read directly into a simulation request.
Simulation does not execute SCHEDULE_EVENT or BROADCAST actions, but it does resolve their payload templates: the action result shows the resolved payload the action would emit, or an error naming the payload field when a template fails.
Simulation enforces the same runtime constraints as live execution: asset action amounts must resolve to positive values (zero or negative amounts fail in the action result), and event numbers or amount results beyond the 2^53 precision boundary are rejected. A clean simulation is a faithful pre-flight check for that rule. It does not prove ordering or stop behavior across multiple rules and sets. See Testing multiple rule sets.
Simulate a draft rule
To test a rule before saving it, usePOST /v1/rules/simulate with a program_id and a rule object (name, condition, actions) plus the same event, participant_id, or participant_state fields. The draft is validated exactly like rule create, but nothing is persisted.