${{ ... }} to compute amounts, counter values, attributes, reference IDs, or dynamic targets.
Operators
Available Variables
When a rule evaluates, these variables are available in your conditions and amount expressions.event
The event_data payload from your API request. Fields depend on what you send, so every expression you write is specific to your event schema.
double():
%) is the exception because CEL requires integers; cast event numbers with int() first.
Numeric magnitude limit
CEL evaluates JSON numbers as 64-bit floats, which are only exact up to a magnitude of 2^53 (9,007,199,254,740,992). To protect ledger precision, event ingestion and rule simulation reject any JSON number inevent_data above that magnitude with 400 amount_precision_exceeded. If you need to carry larger values, send them as strings; string values pass through exactly. The same boundary applies at execution time: an amount expression whose result exceeds 2^53 fails the action rather than posting an imprecise amount.
Optional fields
Not every event has the same shape. If a condition references a field that isn’t in the payload, the condition evaluates tofalse, the rule does not match, and no rule-evaluation row is recorded for that rule. Use has() to check for a field before accessing it:
FAILED.
participant
The participant’s current state at the time of evaluation. Most rules use this to check tags, counters, or attributes before granting rewards.
Identity
Participant identity fields are available alongside state:enrolled_at and created_at are CEL timestamps, so date math works directly without timestamp():
Tags
"x" in participant.tag (and "x" in participant.counter) reflects whether a key is actually set, while participant.tag.<name> returns the default (false / 0) for unset keys, and has(participant.counter.x) is always true. Use in when you need a real presence check.Counters
Use theparticipant.counter.<name> shorthand. A missing counter defaults to 0, so you don’t need get():
participant.counter["q1-2026"].
Counter values are always evaluated numerically, even when the source carried them as decimal strings (the wire format the participant state endpoints return). You can copy a counters map from a state read into a simulation request as-is, with no casting needed.
Attributes
A missing attribute viaparticipant.attribute.<name> is null (no error):
Balances
Useparticipant.balance.<symbol> to read the participant’s available ledger balance for a program-linked asset:
0. An unknown symbol is strict: in a condition it becomes a clean non-match, and in an action expression it fails the action. Even get(participant.balances, "unknown", 0.0) does not substitute the default for an unknown symbol.
Balances are snapshotted at event start, like counters. Actions from earlier rules in the same event are not visible to later rule conditions, so use the projected-balance form above when you need to detect a same-event crossing.
Balance references are also validated when you save a rule. A symbol not linked to the rule’s program is rejected with a 400 such as unknown asset symbol "gems" (linked to program: credits, points). In automation participant_filter and guard_condition expressions, balance references are rejected at save entirely.
program
The program’s current state. Supports the same tags, counters, and attributes as participants. Use this for global logic that isn’t tied to any one participant, like a program-wide redemption cap.
groups
List of groups the participant belongs to. Each entry has id, name, tags, counters, attributes, and tiers, and supports the same counter/tag/attribute shorthand.
A participant can belong to multiple groups, so prefer
groups.exists(g, ...) over assuming a specific list position.now
The event’s event_timestamp as a CEL timestamp. Scrip uses the event timestamp rather than wall-clock time so that evaluation stays deterministic across retries and reprocessing.
Quick Reference
Helper Functions
Scrip addsget(), round(), and the duration_* helpers on top of standard CEL for safe map access, rounding, and time calculations. has() and timestamp() are standard CEL functions, listed here because most rules use them.
get(map, key, default)
Reads a value from a map, returning default if the key doesn’t exist. For counters, tags, and attributes, prefer the dot-access shorthand (participant.counter/tag/attribute.<name>), which bakes in safe defaults. Tiers have no shorthand: read a known tier with participant.tiers.<key>.<field>, and use get() with a default object when the tier may be unset (so the field access stays safe).
round(value, scale)
Rounds a number to the specified decimal places.
duration_hours(duration)
Converts a CEL duration to hours. You get a duration by subtracting two timestamps. Multiply by 60 for minutes.
duration_days(duration)
Converts a CEL duration to days.
duration_weeks(duration)
Converts a CEL duration to weeks.
has(field)
Returns true if a field exists on an object. Use this for event fields that only appear on some event types.
timestamp(string)
Parses an RFC 3339 string (e.g., "2025-06-01T00:00:00Z") into a CEL timestamp for comparison and arithmetic.
Extensions
Scrip enables the CEL math, sets, and strings extension libraries.Math
Min/max capping, absolute value, and rounding. Useful for bounding dynamic amounts to a floor or ceiling.Sets
Membership checks across lists for tag-based conditions.Strings
The strings extension (v4) adds member-style functions on any string value:charAt, indexOf, lastIndexOf, lowerAscii, upperAscii, replace, split, substring, trim, join, format, and reverse, plus the global strings.quote(s).
Expressions in action fields
Action string fields use one explicit convention:
A value is CEL when it is wrapped in ${{ ... }}. Anything else is a literal.
round(105.0 * 0.03, 2) and credits 3.15 points.
Fields that accept ${{ }} include amount, COUNTER value, SET_ATTRIBUTE value, reference_id, target.external_id, and target.participant_id.
Amount results are rounded to the asset’s configured
scale before being applied.
Common Patterns
These patterns come up in most programs. Each one is a complete condition you can use directly or adapt.One-time gate
Grant a reward once, then tag the participant to prevent re-triggering.Threshold crossing
Detect the event that pushes a counter past a target. Counters reflect pre-event state, so check the current value plus the incoming amount.Milestone (Nth occurrence)
Fire on exactly the Nth event of a type. The counter hasn’t incremented yet, so add 1 to get the count including this event.Date range
Restrict a rule to events within a specific window.Days since a date
Check elapsed time between two dates withduration_days.