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

> Group, order, and manage the rules in a program

Every rule belongs to one rule set. A rule set is a program-owned resource that gives related rules a shared execution priority and a local ordering scope.

Rule sets organize execution. They do not enable or disable their rules. Every applicable `ACTIVE` rule across every rule set participates when Scrip processes an event.

```mermaid theme={null}
flowchart TD
    Event[Incoming event]

    subgraph Tracking["1. tracking"]
        direction TB
        T1[Track monthly spend] --> T2[Mark threshold crossing]
    end

    subgraph Earning["2. earning"]
        direction TB
        E1["Dining cashback<br/>stop_after_match: true"]
        E2[Base cashback]
        E1 -->|matches| Skip[Skip rest of this set]
        E1 -->|no match| E2
    end

    subgraph Maintenance["3. maintenance"]
        direction TB
        M1[Monthly reset]
    end

    Event --> Tracking
    Tracking --> Earning
    E2 --> Maintenance
    Skip --> Maintenance
```

This example has three rule sets:

* `tracking` has two rules: Track monthly spend, then Mark threshold crossing.
* `earning` has two rules: Dining cashback, then Base cashback.
* `maintenance` has one rule: Monthly reset.

Scrip runs those sets in that order, and the rules in each set in order. Dining cashback has `stop_after_match: true`, so a match skips Base cashback. Monthly reset still runs because it belongs to `maintenance`.

## The default rule set

Each program has an automatically created rule set with key `default`.

The default set starts at position `1`. Creating another set appends it unless you request a different semantic position.

You can omit `rule_set_id` when you create a rule. Scrip assigns the new rule to the program's `default` set. Omitting the field when you update a rule preserves its current assignment. This keeps single-set integrations compatible while you adopt explicit sets.

<Note>
  The `default` set behaves like any other rule set during execution. Its key does not make it run first. Its `order` determines its position.
</Note>

## Create and inspect rule sets

Create a rule set under its program:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/programs/$PROGRAM_ID/rule-sets \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "earning",
    "name": "Earning rules",
    "position": {"placement": "last"}
  }'
```

The `key` is a stable program-scoped identifier. It must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, or underscores. A rule set's `name` and position can change, but its `key` cannot.

List a program's sets in execution order:

```bash theme={null}
curl https://api.scrip.dev/v1/programs/$PROGRAM_ID/rule-sets \
  -H "Authorization: Bearer $SCRIP_API_KEY"
```

Get or update a set by ID:

```bash theme={null}
curl https://api.scrip.dev/v1/programs/$PROGRAM_ID/rule-sets/$RULE_SET_ID \
  -H "Authorization: Bearer $SCRIP_API_KEY"

curl -X PATCH https://api.scrip.dev/v1/programs/$PROGRAM_ID/rule-sets/$RULE_SET_ID \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Core earning rules"}'
```

Rule sets do not have delete or archive operations. Keep keys stable and use names to describe their current purpose.

See the [Rule sets API reference](/api-reference/rule-sets/overview) for all fields and error responses.

## Assign and move rules

Pass `rule_set_id` when you create a rule:

```json theme={null}
{
  "program_id": "PROGRAM_ID",
  "rule_set_id": "EARNING_RULE_SET_ID",
  "name": "Dining cashback",
  "position": {"placement": "last"},
  "condition": "event.type == 'purchase' && event.mcc == '5812'",
  "actions": [
    {
      "type": "CREDIT",
      "asset_id": "CASHBACK_ASSET_ID",
      "amount": "${{ round(event.amount * 0.05, 2) }}"
    }
  ]
}
```

If you omit `position`, Scrip appends the rule to the selected set. If you omit `rule_set_id`, Scrip selects `default` before assigning the position.

Move an existing rule with the version-guarded move endpoint:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/rules/$RULE_ID/move \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rule_set_id": "EARNING_RULE_SET_ID",
    "position": {"placement": "last"},
    "expected_rule_configuration_version": 7
  }'
```

Read `expected_rule_configuration_version` from the program's [complete configuration](/api-reference/rules/get-rule-configuration). The move compacts the source set and inserts the rule into the destination in one transaction. A stale version returns `409 rule_configuration_version_conflict` without changing either set.

## Execution order

Scrip builds one fixed sequence for each event:

1. Rule sets sort by `rule_set.order`, lowest first.
2. `ACTIVE` rules within each set sort by `rule.order`, lowest first.
3. Matching rule actions persist sequentially in that set and rule order.

All currently applicable `ACTIVE` rules across all sets participate. A lower set position changes when a set runs, not whether another set runs.

Scrip numbers both order fields 1..N with no gaps. `ACTIVE` and `SUSPENDED` rules occupy unique positions within their containing set. Archived rules leave the sequence.

For example:

| Set      | Set order | Rule                    | Rule order |
| -------- | --------- | ----------------------- | ---------- |
| Tracking | 1         | Track monthly spend     | 1          |
| Tracking | 1         | Mark threshold crossing | 2          |
| Earning  | 2         | Dining cashback         | 1          |
| Earning  | 2         | Base cashback           | 2          |

The two tracking rules run before the two earning rules. `Dining cashback` can use `stop_after_match` to skip `Base cashback`, but it cannot skip a tracking rule or a rule in any other set.

### Conflict-safe reordering

Use semantic move endpoints when repositioning one set or rule. Use the atomic reorder endpoints when you have the complete desired list. Both paths require the current `expected_rule_configuration_version`, so a concurrent edit cannot silently overwrite your plan.

Scrip shifts neighboring resources and returns the renumbered sequence. You do not need temporary positions or numeric gaps. See [Managing rule configuration](/guides/managing-rule-configuration) for consistent reads, full-list reorders, and atomic multi-resource changes.

## Set-local stopping

When a matching rule has `stop_after_match: true` and its actions execute successfully, Scrip skips only the remaining rules in that same set. Processing continues with the next set.

This makes one set a useful boundary for mutually exclusive rules. Put dining, grocery, and base earn-rate rules in one `earning` set. A dining match can stop the other earning rules while tracking, maintenance, or notification rules in other sets still participate.

`stop_after_match` does not stop earlier rules. It also does not stop actions that already persisted.

A skipped execution does not trigger the stop. For example, when a rule exceeds its budget, its actions roll back and processing continues with the next rule in the same set.

Two mutually exclusive earn rates in one set:

```json theme={null}
{
  "name": "VIP Double Points",
  "rule_set_id": "earning-rules-uuid",
  "position": {"placement": "first"},
  "stop_after_match": true,
  "condition": "event.type == 'purchase' && 'vip' in participant.tags",
  "actions": [
    {"type": "CREDIT", "asset_id": "...", "amount": "${{ event.amount * 10 }}"}
  ]
}
```

```json theme={null}
{
  "name": "Standard Points",
  "rule_set_id": "earning-rules-uuid",
  "position": {"placement": "last"},
  "condition": "event.type == 'purchase'",
  "actions": [
    {"type": "CREDIT", "asset_id": "...", "amount": "${{ event.amount * 2 }}"}
  ]
}
```

VIPs get 10x because the first rule matches and stops the rest of the set. Everyone else gets 2x because the first rule does not match. Applicable rules in other sets still run for both participants.

## One event-start snapshot

Scrip snapshots participant, program, and group state once at the start of each event, and every condition and dynamic expression in every set reads that same snapshot. A counter incremented by an earlier set is not visible to a later set during the same event. See [State Snapshot Evaluation Behavior](/guides/writing-rules#state-snapshot-evaluation-behavior) for the full behavior and the threshold-crossing pattern.

## Event traces

Event details and impact responses include the execution-time `rule_set_id` on each recorded rule evaluation. Evaluations are returned in the historical set and rule order used for that event.

This trace is not a census of all applicable rules. Clean false conditions have no row. Later rules in the current set that are not reached after a successful `stop_after_match` also have no row, while processing continues with the next set. See [Get an event](/api-reference/events/get-an-event) for skipped-evaluation filtering.

Use the trace to identify the set that contained a rule when it ran. Moving a rule later does not change the recorded set identity for an existing evaluation.

## Simulation boundaries

Single-rule simulation validates one rule's condition and resolved actions. It does not run the program's other rule sets.

A successful single-rule simulation does not prove cross-set ordering, set-local `stop_after_match` behavior, or interactions among matching rules. Send representative events to a dedicated development program when you need the full execution path. See [Testing multiple rule sets](/guides/testing#testing-multiple-rule-sets).

## Recommended structure

Use a small number of sets with distinct responsibilities:

| Key           | Example order | Purpose                                         |
| ------------- | ------------- | ----------------------------------------------- |
| `default`     | 1             | Compatibility for rules that omit `rule_set_id` |
| `tracking`    | 2             | Counters, tags, and other state writes          |
| `earning`     | 3             | Mutually exclusive or cumulative reward rules   |
| `maintenance` | 4             | Reset and lifecycle event handling              |

The [Cashback card example](/examples/cashback-card) shows three named sets executing after an intentionally empty `default` set while the earn-rate rules remain mutually exclusive.
