> ## 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.

# Rule Actions

> Configure what happens when a rule matches

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`, `VOID_HOLD`, `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](/guides/participants#whats-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`)                                                             |
| `VOID_HOLD`      | Cancel HELD lots that were credited directly into HELD (e.g., pending auth rewards), return value to source |
| `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                                                                        |

## Dynamic action fields

Action string fields use `${{ ... }}` for CEL expressions. For most fields, plain values are literals; target `external_id` and `participant_id` still treat unmarked values as legacy bare CEL. Use the explicit form in new rules.

```json theme={null}
{"type": "CREDIT", "asset_id": "...", "amount": "${{ event.amount * 0.03 }}"}
{"type": "COUNTER", "key": "lifetime_spend", "value": "${{ event.amount }}"}
{"type": "SET_ATTRIBUTE", "key": "last_category", "value": "${{ event.category }}"}
{"type": "CREDIT", "asset_id": "...", "amount": "10", "reference_id": "auth-${{ event.authorization_id }}"}
{"type": "CREDIT", "asset_id": "...", "amount": "25", "target": {"external_id": "${{ event.referrer_id }}"}}
```

Whole-string templates keep the expression result type. Mixed templates convert expression results to strings and concatenate them with the surrounding text. See [Expressions in action fields](/guides/cel-expressions#expressions-in-action-fields) for details.

## Asset Actions

### CREDIT

Add funds to an account.

```json theme={null}
{"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., `"365d"`)                                                                                                                                                                                                                                                                                                                                                                                                        |
| `matures_at`   | No       | Lot vesting date (LOT-mode only). RFC 3339 timestamp or duration (e.g., `"30d"`)                                                                                                                                                                                                                                                                                                                                                                                                       |
| `target`       | No       | Alternate recipient (default: event participant)                                                                                                                                                                                                                                                                                                                                                                                                                                       |

For `UNLIMITED` assets, `CREDIT` mints new funds. For `PREFUNDED` assets, `CREDIT` draws from the program wallet.

<Note>
  **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.
</Note>

### DEBIT

Remove funds from an account.

```json theme={null}
{"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](/guides/balance-operations#negative-balances) for details and use cases.

### HOLD

Reserve funds by moving them from `AVAILABLE` to `HELD`.

```json theme={null}
{"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`.

```json theme={null}
{"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`.

```json theme={null}
{"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](/api-reference/participants/forfeit-participant-balance) 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`).

```json theme={null}
{"type": "VOID_HOLD", "asset_id": "uuid", "reference_id": "${{ event.authorization_id }}"}
```

| Field          | Required | Description                                                                                     |
| -------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `asset_id`     | Yes      | Target asset (must be `LOT` mode)                                                               |
| `reference_id` | Yes      | Correlation ID matching the original CREDIT to HELD. Static literal or `${{ }}` CEL expression. |
| `description`  | No       | Context string                                                                                  |

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.

`amount`, `bucket`, and `lot_ids` are not supported: the entire provisional accrual is voided.

## State Actions

<Note>
  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`, or `SET_ATTRIBUTE` `value`) in the current event. See [State Snapshot Evaluation Behavior](/guides/writing-rules#state-snapshot-evaluation-behavior).
</Note>

### TAG

Add a boolean flag to an entity.

```json theme={null}
{"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.

```json theme={null}
{"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.

```json theme={null}
{"type": "COUNTER", "key": "purchase_count", "value": "1"}
{"type": "COUNTER", "key": "lifetime_spend", "value": "${{ event.amount }}"}
{"type": "COUNTER", "key": "monthly_purchases", "value": "1", "reset_after": "30d"}
```

| 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., `"30d"`). 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](/guides/state-management#auto-resetting-counters) for details on auto-reset behavior.

### SET\_ATTRIBUTE

Set a key-value pair on an entity.

```json theme={null}
{"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) |

Use a plain string for literals (`"high"`) and `${{ ... }}` for dynamic values (`"${{ event.category }}"`). Legacy unmarked expressions are still accepted for stored-rule compatibility, but new rules should use the explicit form.

### 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.

```json theme={null}
{"type": "SET_TIER", "tier": "status", "level": "gold"}
{"type": "SET_TIER", "tier": "status", "level": "platinum", "expires_at": "365d"}
```

| Field        | Required | Description                                                            |
| ------------ | -------- | ---------------------------------------------------------------------- |
| `tier`       | Yes      | Tier track identifier (e.g., `"status"`, `"loyalty"`)                  |
| `level`      | Yes      | Tier level within the track (e.g., `"gold"`, `"platinum"`)             |
| `expires_at` | No       | When the tier expires. RFC 3339 timestamp or duration (e.g., `"365d"`) |
| `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:

```json theme={null}
{
  "name": "Gold Tier Promotion",
  "condition": "event.type == 'purchase' && participant.counter.lifetime_spend + event.amount >= 1000.0 && get(participant.tiers, 'status', {'rank': 0}).rank < 2",
  "actions": [
    {"type": "SET_TIER", "tier": "status", "level": "gold", "expires_at": "365d"},
    {"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).

```javascript theme={null}
participant.tiers["status"].rank >= 2
participant.tiers["status"].level == "gold"
participant.tiers["status"].acquired != null &&
duration_hours(now - timestamp(participant.tiers["status"].acquired)) > 720
```

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](/guides/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"`, `"30d"`)                |
| `payload`    | No       | Additional data merged into the scheduled event's `event_data` |

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:

```json theme={null}
{
  "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": "30d"}
  ]
}
```

When the scheduled event fires, it enters the rules engine like any other event. A separate rule handles it:

```json theme={null}
{
  "name": "Inactivity Bonus",
  "condition": "event.type == 'inactivity_check' && participant.counter.purchase_count == 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.

#### Payload templates

String values in `payload` may embed `${{ }}` templates at any nesting depth. 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 payload field path.

```json theme={null}
{
  "type": "SCHEDULE_EVENT",
  "event_name": "settlement_check",
  "delay": "7d",
  "payload": {
    "order_id": "${{ event.order_id }}",
    "projected_points": "${{ event.amount * 0.1 }}",
    "label": "order-${{ event.order_id }}"
  }
}
```

Here `projected_points` arrives in the scheduled event as a JSON number, and `label` as a concatenated string.

### 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.

```json theme={null}
{"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:

```json theme={null}
{
  "name": "Trigger Monthly Bonus",
  "condition": "event.type == 'month_end'",
  "actions": [
    {"type": "BROADCAST", "event_name": "monthly_bonus", "payload": {"month": "2025-01"}}
  ]
}
```

```json theme={null}
{
  "name": "Monthly Bonus Reward",
  "condition": "event.type == 'monthly_bonus' && participant.counter.monthly_purchases >= 5.0",
  "actions": [
    {"type": "CREDIT", "asset_id": "...", "amount": "25"}
  ]
}
```

#### Payload templates

`payload` string values accept the same `${{ }}` templates as [`SCHEDULE_EVENT`](#payload-templates). Templates resolve once, when the triggering rule fires, against the triggering event's context, not per recipient: every participant receives the same resolved payload. A template that errors at runtime fails the whole event.

```json theme={null}
{
  "type": "BROADCAST",
  "event_name": "flash_sale",
  "payload": {
    "sale_id": "${{ event.sale_id }}",
    "bonus_rate": "${{ event.amount * 0.1 }}",
    "label": "sale-${{ event.sale_id }}"
  }
}
```

For more control over fan-out behavior (participant filtering, scheduling, guard conditions), create automations directly via the API. See [Automations](/guides/automations).

<Note>
  `BROADCAST` and `SCHEDULE_EVENT` actions are skipped in test mode. Simulated events do not create automations, and neither simulation nor test runs evaluate payload templates, so a payload template error only surfaces when the rule fires on a live event.
</Note>

## 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:

```json theme={null}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "target": {"type": "PROGRAM"}}
```

```json theme={null}
{"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.

<Warning>
  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' }}"`.
</Warning>

Reference a field from event data (most common):

```json theme={null}
{
  "type": "CREDIT",
  "asset_id": "...",
  "amount": "50",
  "target": {"external_id": "${{ event.referrer_id }}"}
}
```

Use a fixed participant (CEL string literal, note the inner quotes):

```json theme={null}
{
  "type": "CREDIT",
  "asset_id": "...",
  "amount": "50",
  "target": {"external_id": "${{ 'user-123' }}"}
}
```

Look up by Scrip UUID instead of external ID:

```json theme={null}
{
  "type": "CREDIT",
  "asset_id": "...",
  "amount": "50",
  "target": {"participant_id": "${{ event.recipient_id }}"}
}
```

Dynamic target expressions only have access to `event` data and `now`, 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:

```json theme={null}
{
  "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:

```json theme={null}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "expires_at": "365d"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "matures_at": "30d"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "matures_at": "7d", "expires_at": "90d"}
{"type": "CREDIT", "asset_id": "...", "amount": "100", "expires_at": "2025-12-31T23:59:59Z"}
```

Both fields accept RFC 3339 timestamps or duration strings with `h`, `d` (24 hours), and `w` (168 hours) units, combinable (`"1w2d12h"`) with fractional values allowed; months and years are rejected. 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. Expiration and vesting fields are ignored for `SIMPLE`-mode assets.

See [Lots & Expiration](/guides/lots-and-expiration) for details on lot lifecycle.
