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).
| Blocked for inactive participants | Allowed regardless of status |
|---|
CREDIT, DEBIT, HOLD, RELEASE, FORFEIT, COUNTER | TAG, UNTAG, SET_ATTRIBUTE, SET_TIER, BROADCAST, SCHEDULE_EVENT |
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: What’s allowed by status for the full matrix.
Action Types
| Action | Description |
|---|
CREDIT | Add funds to an account |
DEBIT | Remove funds from an account |
HOLD | Move funds from AVAILABLE to HELD |
RELEASE | Move funds from HELD to AVAILABLE |
FORFEIT | Remove funds permanently (to SYSTEM_BREAKAGE) |
TAG | Add a boolean flag |
UNTAG | Remove a boolean flag |
COUNTER | Increment a numeric value |
SET_ATTRIBUTE | Set a key-value string |
SET_TIER | Assign a tier level |
SCHEDULE_EVENT | Create a delayed follow-up event |
BROADCAST | Fan out an event to all participants |
Asset Actions
CREDIT
Add funds to an account.
{"type": "CREDIT", "asset_id": "uuid", "amount": "100"}
{"type": "CREDIT", "asset_id": "uuid", "amount": "event.amount * 10"}
{"type": "CREDIT", "asset_id": "uuid", "amount": "round(event.amount * 0.03, 2)"}
| Field | Required | Description |
|---|
asset_id | Yes | Target asset |
amount | Yes | Static value ("100") or CEL expression ("event.amount * 10") |
bucket | No | Balance bucket: AVAILABLE (default) or HELD |
description | No | Ledger entry description |
reference_id | No | Correlation ID for auth/settle reconciliation (LOT mode only). Behavior depends on the target bucket: crediting to HELD stamps held lots for later release. Crediting to AVAILABLE reconciles against any existing held lots for that reference, handling over/under-capture automatically; if no held lots exist, a standard credit is applied with the reference_id stamped on the lot. Static literal or CEL expression (e.g., "event.authorization_id"). |
expires_at | No | Lot expiration (LOT-mode only). RFC 3339 timestamp or duration (e.g., "8760h") |
matures_at | No | Lot vesting date (LOT-mode only). RFC 3339 timestamp or duration (e.g., "720h") |
target | No | Alternate recipient (default: event participant) |
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.
{"type": "DEBIT", "asset_id": "uuid", "amount": "50"}
| Field | Required | Description |
|---|
asset_id | Yes | Target asset |
amount | Yes | Static value or CEL expression |
allow_negative | No | When true, allows the debit to overdraw the balance below zero. Used for clawbacks and corrections. Defaults to false. Not allowed when target.type is PROGRAM. |
bucket | No | Balance bucket: AVAILABLE (default) or HELD |
target | No | Alternate recipient (default: event participant) |
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.
{"type": "HOLD", "asset_id": "uuid", "amount": "500"}
{"type": "HOLD", "asset_id": "uuid", "amount": "event.amount", "reference_id": "event.authorization_id"}
| Field | Required | Description |
|---|
asset_id | Yes | Target asset |
amount | Yes | Static value or CEL expression |
reference_id | No | Correlation ID to stamp on held lots (LOT mode only). Static literal or CEL expression. |
bucket | No | Source bucket (default: AVAILABLE) |
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.
{"type": "RELEASE", "asset_id": "uuid", "amount": "500"}
{"type": "RELEASE", "asset_id": "uuid", "reference_id": "event.authorization_id"}
| Field | Required | Description |
|---|
asset_id | Yes | Target asset |
amount | Conditional | Static value or CEL expression. Optional when reference_id is provided. Omitting amount releases all lots matching the reference. |
reference_id | No | Release only lots stamped with this reference during a previous hold (LOT mode only). Static literal or CEL expression. |
bucket | No | Source bucket (default: HELD) |
FORFEIT
Remove funds permanently. Debits the participant and credits SYSTEM_BREAKAGE.
{"type": "FORFEIT", "asset_id": "uuid", "amount": "100", "bucket": "AVAILABLE"}
| Field | Required | Description |
|---|
asset_id | Yes | Target asset |
amount | Yes | Static value or CEL expression |
bucket | No | Source bucket (default: AVAILABLE) |
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.
State Actions
TAG
Add a boolean flag to an entity.
{"type": "TAG", "tag": "VIP"}
{"type": "TAG", "tag": "FIRST_PURCHASE"}
| Field | Required | Description |
|---|
tag | Yes | Tag name to add |
target | No | Alternate recipient (default: event participant) |
UNTAG
Remove a boolean flag from an entity.
{"type": "UNTAG", "tag": "PROMO_ACTIVE"}
{"type": "UNTAG", "tag": "INTRO_PERIOD"}
| Field | Required | Description |
|---|
tag | Yes | Tag name to remove |
target | No | Alternate recipient (default: event participant) |
Removing a tag that doesn’t exist is a no-op. The action succeeds silently.
COUNTER
Increment a numeric value.
{"type": "COUNTER", "key": "purchase_count", "value": "1"}
{"type": "COUNTER", "key": "lifetime_spend", "value": "event.amount"}
{"type": "COUNTER", "key": "monthly_purchases", "value": "1", "reset_after": "720h"}
| Field | Required | Description |
|---|
key | Yes | Counter name |
value | Yes | Static number or CEL expression to add to the current value |
reset_after | No | Auto-reset duration (e.g., "720h"). Counter resets to 0 when the duration elapses. Send empty string to remove auto-reset. |
target | No | Alternate recipient (default: event participant) |
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.
{"type": "SET_ATTRIBUTE", "key": "spender_tier", "value": "high"}
{"type": "SET_ATTRIBUTE", "key": "last_category", "value": "event.category"}
| Field | Required | Description |
|---|
key | Yes | Attribute name |
value | Yes | Static string or CEL expression |
target | No | Alternate recipient (default: event participant) |
The system decides whether to evaluate value as a CEL expression by scanning for syntax characters: ., (), operators (+, *, ==, etc.), and brackets. A value like "high" contains none of these and is stored as a literal string. A value like "event.category" contains . and is evaluated as CEL against the full event and participant context. If a value triggers CEL detection but the expression is invalid, the action fails.
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.
{"type": "SET_TIER", "tier": "status", "level": "gold"}
{"type": "SET_TIER", "tier": "status", "level": "platinum", "expiry": "8760h"}
| Field | Required | Description |
|---|
tier | Yes | Tier track identifier (e.g., "status", "loyalty") |
level | Yes | Tier level within the track (e.g., "gold", "platinum") |
expiry | No | When the tier expires. RFC 3339 timestamp or duration (e.g., "8760h") |
target | No | Alternate recipient (default: event participant) |
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:
{
"name": "Gold Tier Promotion",
"condition": "event.type == 'purchase' && get(participant.counters, 'lifetime_spend', 0.0) + event.amount >= 1000.0 && get(participant.tiers, 'status', {'rank': 0}).rank < 2",
"actions": [
{"type": "SET_TIER", "tiers": "status", "level": "gold", "expiry": "8760h"},
{"type": "COUNTER", "key": "lifetime_spend", "value": "event.amount"}
]
}
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).
participant.tiers["status"].rank >= 2
participant.tiers["status"].level == "gold"
If expiry 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 under the hood. SCHEDULE_EVENT creates a one-time automation scoped to the program, and BROADCAST creates an immediate automation that fans out to all participants.
SCHEDULE_EVENT
Create a follow-up event that fires after a specified delay. Under the hood, this creates a one_time + program automation targeting the same participant who triggered the original rule.
| Field | Required | Description |
|---|
event_name | Yes | The type value for the scheduled event’s event_data |
delay | Yes | Duration before firing (e.g., "24h", "720h") |
payload | No | Additional data merged into the scheduled event’s event_data |
Common durations: 24h (1 day), 168h (1 week), 720h (30 days), 8760h (1 year).
This rule grants a signup bonus and schedules an inactivity check 30 days later:
{
"name": "Schedule Inactivity Check",
"condition": "event.type == 'signup'",
"actions": [
{"type": "CREDIT", "asset_id": "...", "amount": "50"},
{"type": "TAG", "tag": "WELCOME_BONUS"},
{"type": "SCHEDULE_EVENT", "event_name": "inactivity_check", "delay": "720h"}
]
}
When the scheduled event fires, it enters the rules engine like any other event. A separate rule handles it:
{
"name": "Inactivity Bonus",
"condition": "event.type == 'inactivity_check' && get(participant.counters, 'purchase_count', 0.0) == 0.0",
"actions": [
{"type": "CREDIT", "asset_id": "...", "amount": "25"}
]
}
The automation is deduplicated using a key derived from the triggering event, rule, participant, event name, delay, and payload. Replaying the same event does not create a duplicate automation.
BROADCAST
Fan out an event to every active participant in the program. Under the hood, this creates an immediate + participants automation that begins fan-out right away.
{"type": "BROADCAST", "event_name": "monthly_bonus", "payload": {"bonus_amount": 50}}
| Field | Required | Description |
|---|
event_name | Yes | The type value for the broadcast event’s event_data |
payload | No | Additional data merged into each participant’s event |
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:
{
"name": "Trigger Monthly Bonus",
"condition": "event.type == 'month_end'",
"actions": [
{"type": "BROADCAST", "event_name": "monthly_bonus", "payload": {"month": "2025-01"}}
]
}
{
"name": "Monthly Bonus Reward",
"condition": "event.type == 'monthly_bonus' && get(participant.counters, 'monthly_purchases', 0.0) >= 5.0",
"actions": [
{"type": "CREDIT", "asset_id": "...", "amount": "25"}
]
}
For more control over fan-out behavior (participant filtering, scheduling, guard conditions), create automations directly via the API. See Automations.
BROADCAST and SCHEDULE_EVENT actions are skipped in test mode. Simulated events do not create automations.
Targeting
By default, actions apply to the event’s participant. Use target to route an action to a different entity.
These action types support target: CREDIT, DEBIT, TAG, UNTAG, COUNTER, SET_ATTRIBUTE, SET_TIER.
Static Targets
Target the program itself or a specific group by ID:
{"type": "CREDIT", "asset_id": "...", "amount": "100", "target": {"type": "PROGRAM"}}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "target": {"type": "GROUP", "id": "group-uuid"}}
Dynamic Targets
Resolve the target from event data using a CEL expression. Use external_id to look up a participant by your application’s user ID, or participant_id to look up by Scrip UUID.
The external_id and participant_id fields in target are CEL expressions, not plain strings. To reference a value from the event payload, write event.field_name. To use a literal string, wrap it in quotes: '"user-123"'.
Reference a field from event data (most common):
{
"type": "CREDIT",
"asset_id": "...",
"amount": "50",
"target": {"external_id": "event.referrer_id"}
}
Use a fixed participant (CEL string literal, note the inner quotes):
{
"type": "CREDIT",
"asset_id": "...",
"amount": "50",
"target": {"external_id": "'user-123'"}
}
Look up by Scrip UUID instead of external ID:
{
"type": "CREDIT",
"asset_id": "...",
"amount": "50",
"target": {"participant_id": "event.recipient_id"}
}
Dynamic target expressions only have access to event data, not participant state or program state. Only one ID field (external_id, participant_id, or id) can be specified per target. The target participant must exist and be enrolled in the program.
Example: Referral Bonus
Credit both the new user and their referrer from a single event:
{
"name": "Referral Bonus",
"condition": "event.type == 'signup' && has(event.referrer_id)",
"actions": [
{"type": "CREDIT", "asset_id": "...", "amount": "50"},
{"type": "CREDIT", "asset_id": "...", "amount": "25", "target": {"external_id": "event.referrer_id"}},
{"type": "COUNTER", "key": "referral_count", "value": "1", "target": {"external_id": "event.referrer_id"}}
]
}
Lot Expiration and Vesting
For LOT-mode assets, CREDIT actions can set expiration and vesting dates:
{"type": "CREDIT", "asset_id": "...", "amount": "100", "expires_at": "8760h"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "matures_at": "720h"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "matures_at": "168h", "expires_at": "2160h"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "expires_at": "2025-12-31T23:59:59Z"}
Both fields accept RFC 3339 timestamps or Go durations. These fields are ignored for SIMPLE-mode assets.
See Lots & Expiration for details on lot lifecycle.