Skip to main content
Scrip uses CEL (Common Expression Language) for rule conditions and dynamic action fields. You write conditions to control when a rule fires, and you wrap action-field expressions in ${{ ... }} to compute amounts, counter values, attributes, reference IDs, or dynamic targets.

Operators

Use == for equality, not =. Using = is a syntax error.

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.
Numeric values in your payload work directly in normal arithmetic and comparisons:
If a numeric value is sent as a string in your payload, cast it with double():
Modulo (%) 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 in event_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 to false, 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:
If a matched rule’s action expression reads a missing field, the behavior is different: the action fails and the event is marked 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():
For program-scoped events with no participant, identity values are absent and a condition that reads them is a clean non-match.

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 the participant.counter.<name> shorthand. A missing counter defaults to 0, so you don’t need get():
Counter keys that aren’t valid identifiers (hyphens, leading digits, spaces) need bracket access: 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 via participant.attribute.<name> is null (no error):

Balances

Use participant.balance.<symbol> to read the participant’s available ledger balance for a program-linked asset:
Every asset linked to the program is loaded with a real value. A linked but unfunded asset reads as 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 adds get(), 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).
Don’t index the plural maps directly for a key that may be missing (participant.counters["spend"]); use the shorthand or get(), and see Optional fields for missing-key behavior.

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.
In this example, a $105 purchase evaluates 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.
The ${{ ... }} wrapper is what marks a value as an expression. For string literals that contain operator characters, wrap a quoted string expression, such as "value": "${{ 'us-west' }}".

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 with duration_days.

Capped bonus

Reward a percentage of the event amount but cap the maximum payout.

Category-specific

Only match events in certain categories.

Segment-specific

Use tags to restrict rules to a participant segment.

Exclude already-rewarded

Prevent a participant from claiming a promotion more than once.

Common Gotchas