Documentation Index
Fetch the complete documentation index at: https://docs.scrip.dev/llms.txt
Use this file to discover all available pages before exploring further.
Scrip uses CEL (Common Expression Language) for rule conditions and dynamic amount calculations. You write conditions to control when a rule fires and expressions to compute how much to credit, debit, or hold. Conditions return a boolean. Amount expressions return a number.
// Condition (returns boolean)
event.type == "purchase" && event.amount > 50.0
// Amount (returns number)
event.amount * 0.10
Operators
| Operator | Example |
|---|
| Equality | event.type == "purchase" |
| Inequality | event.type != "refund" |
| Comparison | event.amount > 100.0, event.amount >= 50.0 |
| Logical AND | event.type == "purchase" && event.amount > 0 |
| Logical OR | event.category == "dining" || event.category == "travel" |
| Negation | !("vip" in participant.tags) |
| Containment | "vip" in participant.tags |
| List membership | event.category in ["dining", "travel"] |
| Ternary | event.tier == "gold" ? 100.0 : 50.0 |
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.
event.type == "purchase"
event.amount > 50.0
event.category in ["dining", "travel", "entertainment"]
event.items.size() > 0
event.items[0].sku == "ABC123"
Numeric values in your payload work directly in arithmetic:
event.amount * 0.10 // works, amount is already a number
event.quantity * 5.0 // works
If a numeric value is sent as a string in your payload, cast it with double():
double(event.amount) * 0.10 // only needed if amount is "49.99" not 49.99
Optional fields
Not every event has the same shape. If a condition references a field that isn’t in the payload, the entire condition evaluates to false and the rule is skipped. Use has() to check for a field before accessing it:
has(event.referrer_id) && event.referrer_id != ""
has(event.coupon_code) && event.coupon_code == "SUMMER25"
participant
The participant’s current state at the time of evaluation. Most rules use this to check tags, counters, or attributes before granting rewards.
participant.tags // list of strings
participant.counters // map of string to number
participant.attributes // map of string to string
participant.tiers // map of string to tier state
"vip" in participant.tags
!("welcome_bonus" in participant.tags)
Counters
Use get() to read counter values with a default. If the counter hasn’t been set, get() returns the default instead of failing.
get(participant.counters, "purchase_count", 0.0) >= 10.0
get(participant.counters, "lifetime_spend", 0.0) > 1000.0
Attributes
get(participant.attributes, "region", "") == "US"
get(participant.attributes, "plan", "free") in ["pro", "enterprise"]
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.
program.id // UUID string
"milestone_reached" in program.tags
get(program.counters, "total_issued", 0.0) < 100000.0
get(program.attributes, "region", "") == "US"
groups
List of groups the participant belongs to. Each entry has id, name, tags, counters, attributes, and tiers.
groups.size() > 0
groups.size() > 0 && groups[0].name == "VIP Customers"
groups.size() > 0 && get(groups[0].counters, "team_purchases", 0.0) < 100.0
A participant can belong to multiple groups, so groups is a list. If your program uses one group per participant, groups[0] works as a shorthand.
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.
now > timestamp("2025-01-01T00:00:00Z")
now < timestamp("2025-12-31T23:59:59Z")
Quick Reference
| Data | Pattern |
|---|
| Event field (number) | event.amount > 50.0 |
| Event field (string) | event.type == "purchase" |
| Optional event field | has(event.referrer_id) && event.referrer_id != "" |
| Tag check | "vip" in participant.tags |
| Counter | get(participant.counters, "key", 0.0) >= 10.0 |
| Attribute | get(participant.attributes, "key", "") == "US" |
| Timestamp comparison | now > timestamp("2025-01-01T00:00:00Z") |
| Program counter | get(program.counters, "key", 0.0) < 1000.0 |
Helper Functions
Scrip adds a few helpers on top of standard CEL for safe map access, rounding, and time calculations.
get(map, key, default)
Reads a value from a map, returning default if the key doesn’t exist. This is the most common helper in practice. Always use get() for counters and attributes, since accessing a missing key directly causes the entire condition to evaluate to false, which can prevent the other side of an || expression from evaluating.
get(participant.counters, "spend", 0.0) > 100.0
get(participant.attributes, "plan", "") in ["pro", "enterprise"]
round(value, scale)
Rounds a number to the specified decimal places.
round(event.amount * 0.03, 2) // 49.99 * 0.03 = 1.4997 → 1.50
round(event.amount, 0) // 49.99 → 50.0
duration_hours(duration)
Converts a CEL duration to hours. You get a duration by subtracting two timestamps. Divide by 24 for days, multiply by 60 for minutes.
// More than 30 days since activation
duration_hours(now - timestamp(event.activation_date)) / 24.0 > 30.0
// Less than 1 hour old
duration_hours(now - timestamp(event.created_at)) < 1.0
has(field)
Returns true if a field exists on an object. Use this for event fields that only appear on some event types.
// Only match if the event includes a coupon code
has(event.coupon_code) && event.coupon_code == "SUMMER25"
timestamp(string)
Parses an RFC 3339 string (e.g., "2025-06-01T00:00:00Z") into a CEL timestamp for comparison and arithmetic.
timestamp(event.purchase_date) >= timestamp("2025-06-01T00:00:00Z")
now - timestamp(event.activation_date) // produces a duration
Extensions
Scrip enables the CEL math and sets extension libraries.
Math
Min/max capping, absolute value, and rounding. Useful for bounding dynamic amounts to a floor or ceiling.
math.least(event.amount * 0.1, 50.0) // cap: 10% of amount, max 50
math.greatest(event.amount * 0.05, 10.0) // floor: 5% of amount, min 10
math.abs(-42.0) // 42.0
math.ceil(3.2) // 4.0
math.floor(3.8) // 3.0
Sets
Membership checks across lists for tag-based conditions.
sets.contains(participant.tags, ["vip", "gold"]) // participant has ALL of these tags
sets.intersects(participant.tags, ["vip", "silver"]) // participant has ANY of these tags
Dynamic Amounts
Rule actions that move assets (CREDIT, DEBIT, HOLD, etc.) have an amount field. You can set it to a static number or a CEL expression. If the value parses as a number, it’s used directly. Otherwise, Scrip evaluates it as a CEL expression against the event data.
{
"name": "Cashback Reward",
"condition": "event.type == 'purchase'",
"actions": [
{
"type": "CREDIT",
"asset_id": "...",
"amount": "round(event.amount * 0.03, 2)"
}
]
}
In this example, a $105 purchase evaluates round(105.0 * 0.03, 2) and credits 3.15 points.
| Amount value | Behavior |
|---|
"100" | Static. Credits exactly 100. |
"event.amount * 0.10" | 10% of the event amount |
"round(event.amount * 0.03, 2)" | 3% of the event amount, rounded to 2 decimal places |
"math.least(event.amount * 0.1, 50.0)" | 10% of the event amount, capped at 50 |
"math.greatest(event.amount * 0.05, 10.0)" | 5% of the event amount, minimum 10 |
"event.tier == 'gold' ? 100.0 : 50.0" | 100 for gold tier, 50 for everyone else |
The result is 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.
event.type == "signup" && !("welcome_bonus" in participant.tags)
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.
get(participant.counters, "spend", 0.0) < 1000.0 &&
(get(participant.counters, "spend", 0.0) + event.amount) >= 1000.0
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.
event.type == "ride_complete" &&
(get(participant.counters, "ride_count", 0.0) + 1.0) == 10.0
Date range
Restrict a rule to events within a specific window.
timestamp(event.purchase_date) >= timestamp("2025-06-01T00:00:00Z") &&
timestamp(event.purchase_date) < timestamp("2025-07-01T00:00:00Z")
Days since a date
Check elapsed time between two dates. duration_hours returns hours, so divide by 24 for days.
duration_hours(now - timestamp(event.activation_date)) / 24.0 <= 30.0
Capped bonus
Reward a percentage of the event amount but cap the maximum payout.
math.least(event.amount * 0.1, 50.0)
Category-specific
Only match events in certain categories.
event.type == "purchase" && event.category in ["dining", "travel"]
Segment-specific
Use tags to restrict rules to a participant segment.
event.type == "purchase" && "vip" in participant.tags
Exclude already-rewarded
Prevent a participant from claiming a promotion more than once.
event.type == "purchase" && !("promo_claimed" in participant.tags)
Common Gotchas
| Mistake | Fix |
|---|
event.type = "purchase" | Use == not = |
event.amount when amount is a string | Use double(event.amount) |
Referencing event.field that may not exist | Condition returns false. Use has(event.field) if the rule should still match. |
| Checking state updated earlier in the same event | Conditions use the event-start snapshot. For counters, use the threshold crossing or Nth occurrence pattern. For tags, attributes, or tiers, put the dependent action in the same rule or trigger it on a later event. See State Snapshot Evaluation Behavior. |