Skip to main content
Actions define what happens when a rule’s condition matches. Each rule has one or more actions that execute atomically in a single transaction.

Participant status restrictions

Not all actions are allowed on every participant. Financial actions are blocked for SUSPENDED and CLOSED participants to prevent inactive accounts from accumulating value. Metadata actions are always allowed so you can still manage inactive accounts (e.g., tagging for audit, updating attributes during a review). If a rule triggers a blocked action, the action fails and the event is marked FAILED. If matching rules only trigger allowed actions, the event completes normally. See Participants: Allowed actions by status for the full matrix.

Action types

Dynamic action fields

Action string fields use ${{ ... }} for CEL expressions, and plain values are literals. One exception: target external_id and participant_id evaluate unwrapped values as CEL, so always wrap expressions in ${{ }} and write fixed IDs as quoted string expressions.
Whole-string templates keep the expression result type. Mixed templates convert expression results to strings and concatenate them with the surrounding text. Every action expression, including a target, uses the same variables and state snapshot as the rule condition, taken when processing starts. See Expressions in action fields for details.

Asset actions

CREDIT

Add funds to an account.
For UNLIMITED assets, CREDIT mints new funds. For PREFUNDED assets, CREDIT draws from the program wallet.
Automatic rounding: If the evaluated amount has more decimal places than the asset’s scale, the value is rounded automatically. For example, ${{ event.amount * 0.03 }} might produce 1.009, which rounds to "1.01" on a scale: 2 asset. No error is returned. Use round() in your CEL expression if you need explicit control.

DEBIT

Remove funds from an account.
Fails with insufficient balance if the available balance is less than the requested amount, unless allow_negative is true. See Balance Operations: Negative Balances for details and use cases.

HOLD

Reserve funds by moving them from AVAILABLE to HELD.
Held funds are not spendable. Use for authorization holds or pending settlements. When reference_id is provided, the held lots are stamped so a future RELEASE can target them by reference.

RELEASE

Move funds from HELD back to AVAILABLE.

FORFEIT

Remove funds permanently. Debits the participant and credits SYSTEM_BREAKAGE.
Use for point expiration or policy violations. Rule-triggered forfeits are blocked for non-active participants. Use the forfeit API endpoint for manual cleanup of closed accounts.

VOID_HOLD

Cancel HELD lots that were credited directly into the HELD bucket (not moved there from AVAILABLE via a HOLD action). Returns value to the original source account (program wallet for PREFUNDED, SYSTEM_ISSUANCE for UNLIMITED).
Use for auth reversals, when a merchant voids a transaction before settlement. Lots that a participant already owned and moved to HELD via a HOLD action are excluded, so you cannot accidentally void participant-owned funds. Set amount for a partial reversal. It is the rewards to cancel, not the reversed purchase amount. Calculate it as the rewards for the amount authorized before this reversal minus the rewards for the amount still authorized. See how to calculate amount for a worked example. Scrip voids the newest pending rewards first and leaves the rest in HELD for the settlement. If amount is more than what is left, Scrip voids what is left. The amount is rounded to the asset’s scale like a CREDIT amount. An amount that rounds to zero is skipped. A zero or negative amount fails the action. A blank amount is rejected when you create or update the rule, and an expression that resolves to blank fails the action. Neither voids all pending rewards. See Void hold. bucket and lot_ids are not supported.

State actions

State actions are durable updates, not same-event condition inputs. If a rule adds a tag, increments a counter, sets an attribute, or assigns a tier, the new state is visible to later events. It is not visible to lower-order rule conditions or to dynamic action expressions (like amount, COUNTER value, SET_ATTRIBUTE value, or a dynamic target) in the current event. See State snapshot evaluation.

TAG

Add a boolean flag to an entity.

UNTAG

Remove a boolean flag from an entity.
Removing a tag that doesn’t exist is a no-op. The action succeeds silently.

COUNTER

Increment a numeric value.
The value field increments the counter. It does not replace it. See State Management for details on auto-reset behavior.

SET_ATTRIBUTE

Set a key-value pair on an entity.
Use a plain string for literals ("high") and ${{ ... }} for dynamic values ("${{ event.category }}").

SET_TIER

Assign a tier level on an entity. Each tier represents a separate track (e.g., "status", "loyalty"), and level specifies a position within that track. Tier levels have a numeric rank that defines their order in the hierarchy.
If the participant already holds a tier in the same track, SET_TIER overwrites it. The previous tier is recorded as a transition for audit purposes. This rule promotes a participant to gold when their lifetime spend crosses $1,000:
Tier state is available in CEL via participant.tiers. Each entry is a map with level (string), rank (number), benefits (map), acquired (timestamp string), and expires (timestamp string or null).
If expires_at is set, the system schedules a tier_expiration event when the duration elapses. That event enters the rules engine and triggers the tier’s configured downgrade policy.

Scheduling actions

These actions create automations: stored event submissions that Scrip fires on a trigger. SCHEDULE_EVENT creates a one-time program-scoped automation that fires after a delay, and BROADCAST creates a one-time participants-scoped automation that fans out immediately. Both attach an event_data object with the same semantics as POST /v1/events: it is submitted verbatim when the automation fires, so include type in it yourself when the receiving rule routes on event.type.

SCHEDULE_EVENT

Create a follow-up event that fires after a delay. By default the follow-up event goes to the same participant. Set target to send it to a different participant or to the program. Scrip stores the pending event as a one_time automation with source: rule_action. Common durations: 1d or 24h (1 day), 7d (1 week), 30d (30 days), 365d (1 year). This rule grants a signup bonus and schedules an inactivity check 30 days later:
When the follow-up event fires, it enters the rules engine like any other event. A separate rule handles it:
Each triggering event schedules a given follow-up only once. Replaying the event does not schedule it again, even if the data a target reads has changed since the first run. Two SCHEDULE_EVENT actions with different targets each schedule their own follow-up. With target, Scrip looks up the recipient when the rule runs, and the recipient must be enrolled in the program. When the follow-up event fires, your rules see it as the recipient’s event: participant holds the recipient’s counters, tags, and tiers, not those of the participant who triggered it. With {"type": "PROGRAM"}, the follow-up event has no participant. These two rules pay a referrer at most five times. The first sends a referral_payout event to the referrer named in the buyer’s referred_by attribute. The second runs on that event, so it can check the referrer’s own payout count:

BROADCAST

Fan out an event to every active participant in the program. This stores a one_time + participants automation (source: rule_action) with no trigger_at, so it begins fan-out as soon as Scrip picks it up.
BROADCAST cannot include target, asset_id, amount, or other action-specific fields. The broadcast event itself triggers rules, and those rules define what happens. This pair of rules runs a conditional monthly bonus. The first rule fires the broadcast; the second rule runs for each participant who qualifies:
For more control over fan-out behavior (participant filtering, scheduling, guard conditions), create automations directly via the API. See Automations.

Event data templates

String values in event_data may embed ${{ }} templates at any nesting depth, for both actions. Templates resolve when the triggering rule fires, against the triggering event’s context. A whole-string template keeps the expression’s type (numbers arrive as JSON numbers); mixed text and template stringifies the result. Object keys are never evaluated. A template that errors at runtime fails the whole event, the same as a bad amount expression. A whole-string template that evaluates to CEL null becomes JSON null; in mixed text, null contributes an empty string. Numeric results must be finite and within the precision-safe range (magnitude at or below 2^53). Results outside that range, NaN, and non-JSON-encodable values fail the action with an error naming the event_data field path.
Here projected_points arrives in the follow-up event as a JSON number, and label as a concatenated string. For BROADCAST, templates resolve once, against the triggering event’s context, not per recipient: every participant receives the same resolved event_data.
BROADCAST and SCHEDULE_EVENT actions do not run in test mode, but single-rule simulations and rule configuration simulations still show what they would do. The action result includes the event_data that would be sent, or the field whose template failed. For SCHEDULE_EVENT it also shows who would receive the event: resolved_target in a single-rule simulation, or target on the skipped_action_details entry in a rule configuration simulation.

Targeting

By default, actions apply to the event’s participant. Use target to apply an action to a different participant, a group, or the program. These action types support target: CREDIT, DEBIT, TAG, UNTAG, COUNTER, SET_ATTRIBUTE, SET_TIER, and SCHEDULE_EVENT. SCHEDULE_EVENT can target a participant or the program, but not a group. See SCHEDULE_EVENT for how the follow-up event is delivered.

Static targets

Target the program itself or a specific group by ID:

Dynamic targets

A dynamic target finds the participant with an expression, so the recipient can change from event to event. The expression can read the same variables as the rule condition: event, participant, state, program, groups, recipient, and now. Use external_id to find a participant by your app’s user ID, or participant_id to find them by their Scrip ID.
The external_id and participant_id fields in target should use ${{ ... }} for expressions. To use a literal string with these expression-only target fields, wrap a string literal inside the template: "${{ 'user-123' }}".
Reference a field from event data (most common):
Use a fixed participant (CEL string literal, note the inner quotes):
Look up by Scrip UUID instead of external ID:
Read the target from the participant’s own data, such as a referrer ID saved as an attribute when they signed up:
If the expression returns null or an empty string, the action fails and the whole event fails with it. When the attribute might be missing, check for it in the condition, for example event.type == 'first_purchase' && participant.attribute.referred_by != null. A target reads the participant’s data as it was when processing started. If an earlier action in the same event sets referred_by, the target still sees the old value. See State snapshot evaluation. Set only one of external_id, participant_id, or id per target. The participant must exist and be enrolled in the program. When you save a rule, Scrip checks target expressions the same way it checks conditions, so a misspelled participant field or an unknown asset symbol is rejected. When you simulate a rule, each targeted action shows who it would reach in resolved_target, for example {"external_id": "user_123"}. groups is not available in simulation, so a target that reads groups shows an error there instead.

Example: referral bonus

Credit both the new user and their referrer from a single event:

Lot expiration and vesting

For LOT-mode assets, CREDIT actions can set expiration and vesting dates:
Both fields accept an RFC 3339 timestamp, a positive duration string, or a ${{ }} expression that resolves to either form. Durations count from processing time and use the Go units (s, m, h, and smaller) plus d (24 hours) and w (168 hours), combinable ("1w2d12h") with fractional values allowed; months and years are rejected. For occurrence-based or exact calendar deadlines, compute an RFC 3339 timestamp in your backend and resolve it from event_data. The same units work in SET_TIER expires_at, SCHEDULE_EVENT delay, COUNTER reset_after, and budget interval. The same format applies to tier lifecycle retention.duration and automation schedule_config durations. On a SIMPLE-mode asset, a rule that sets either field is rejected at save with invalid_inventory_mode. See Lots & Expiration for details on lot lifecycle.