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

# State Management

> Tags, counters, and attributes for tracking participant state

Tags, counters, attributes, and tiers store state on participants, programs, and groups. Rules read this state in CEL conditions and update it through actions.

Tiers are covered in their own guide. See [Tiers](/guides/tiers) for qualification, retention, and downgrade policies. Ledger balances are separate from state, but conditions can read them through `participant.balance.<symbol>`. See [CEL Expressions](/guides/cel-expressions#balances).

## Tags

Boolean flags. A tag either exists on an entity or it doesn't. Useful for one-time gates, status markers, and segmentation labels.

```bash theme={null}
# Add a tag
PUT /v1/participants/{id}/state/tags/vip

# Remove a tag
DELETE /v1/participants/{id}/state/tags/vip

# List all tags
GET /v1/participants/{id}/state/tags
```

Tags are trimmed and de-duplicated on write, stored verbatim, and matched case-sensitively in CEL: `"VIP"` and `"vip"` are different tags. Pick one casing convention and use it consistently; these docs use lowercase.

### Using tags in conditions

```javascript theme={null}
// Check if tagged
"vip" in participant.tags

// One-time gate
!("welcome_bonus" in participant.tags)

// Multiple tags
sets.contains(participant.tags, ["vip", "gold"])      // has ALL
sets.intersects(participant.tags, ["vip", "silver"])   // has ANY
```

### Setting tags from rules

The `TAG` action adds a tag and `UNTAG` removes one. Both apply to the event's participant by default.

This rule grants a one-time welcome bonus and tags the participant to prevent re-triggering:

```json theme={null}
{
  "name": "Welcome Bonus",
  "condition": "event.type == 'signup' && !('welcome_bonus' in participant.tags)",
  "actions": [
    {"type": "CREDIT", "asset_id": "...", "amount": "100"},
    {"type": "TAG", "tag": "welcome_bonus"}
  ]
}
```

Use `UNTAG` to remove a tag when a condition is no longer met. This rule removes a promotional flag when the promo period ends:

```json theme={null}
{"type": "UNTAG", "tag": "promo_active"}
```

To tag or untag a different entity, add a `target`:

```json theme={null}
{"type": "TAG", "tag": "milestone_reached", "target": {"type": "PROGRAM"}}
{"type": "UNTAG", "tag": "milestone_reached", "target": {"type": "PROGRAM"}}
```

## Counters

Numeric accumulators. Stored as high-precision decimals, available as numbers in CEL expressions. Useful for running totals, occurrence counts, and threshold tracking.

```bash theme={null}
# Set a counter
PUT /v1/participants/{id}/state/counters/lifetime_spend
{"value": "500.00"}

# Increment a counter
POST /v1/participants/{id}/state/counters/lifetime_spend/increment
{"delta": "49.99"}

# Get a counter
GET /v1/participants/{id}/state/counters/lifetime_spend

# Delete a counter
DELETE /v1/participants/{id}/state/counters/lifetime_spend
```

`PUT` sets an absolute value and is last-write-wins; `increment` adds `delta` atomically, so concurrent writers never lose updates, and negative deltas decrement. See [Increment participant counter](/api-reference/participants/increment-participant-counter).

### Using counters in conditions

Prefer the dot-access shorthand: a missing counter defaults to `0`, so no `get()` is needed:

```javascript theme={null}
// Simple threshold
participant.counter.purchase_count >= 10

// Threshold crossing detection
participant.counter.spend < 1000.0 &&
(participant.counter.spend + event.amount) >= 1000.0
```

### Updating counters from rules

The `COUNTER` action increments the current value. It does not replace it. This rule tracks purchase count and lifetime spend on every purchase:

```json theme={null}
{
  "name": "Track Purchase Activity",
  "condition": "event.type == 'purchase'",
  "actions": [
    {"type": "COUNTER", "key": "purchase_count", "value": "1"},
    {"type": "COUNTER", "key": "lifetime_spend", "value": "${{ event.amount }}"}
  ]
}
```

If a participant has `purchase_count: 7` and `lifetime_spend: 340.00`, and a purchase event arrives with `event.amount: 49.99`, after this rule fires:

* `purchase_count` becomes `8`
* `lifetime_spend` becomes `389.99`

The `value` field accepts static values (`"1"`) or `${{ }}` CEL expressions (`"${{ event.amount }}"`).

<Warning>
  Counters reflect the pre-event state during rule evaluation. If Rule A increments a counter, Rule B in the same event still sees the original value. The same applies to tags, attributes, and tiers: state actions are durable updates, not same-event condition inputs. See [State Snapshot Evaluation Behavior](/guides/writing-rules#state-snapshot-evaluation-behavior) for details.
</Warning>

### Auto-resetting counters

Counters can be configured to automatically reset to 0 after a duration elapses. The timer is per-participant, starting from when the counter was last created, explicitly set, or auto-reset. Resets are applied on the next read or write, not at the exact moment the timer expires.

Set `reset_after` via the API:

```bash theme={null}
PUT /v1/participants/{id}/state/counters/monthly_purchases
{"value": "1", "reset_after": "30d"}
```

Or from a rule action:

```json theme={null}
{"type": "COUNTER", "key": "monthly_purchases", "value": "1", "reset_after": "30d"}
```

The duration accepts the Go units `s`, `m`, and `h` plus `d` (days) and `w` (weeks), combinable as in `"1w2d12h"`; `"30d"` and `"720h"` are equivalent. Note that `m` means minutes, not months.

Each participant gets their own independent timer. Reading the counter returns the effective value (0 if the window has elapsed), along with `reset_after` and `last_reset_at` when auto-reset is configured.

To remove auto-reset from a counter, set `reset_after` to an empty string. Omitting the field leaves the existing configuration unchanged.

This is for per-participant elapsed-time windows: "reset purchase count 30 days after first purchase" or "daily counter that resets after 24h of inactivity." For calendar-aligned resets (every Sunday, 1st of month), use a cron [automation](/guides/automations) instead.

## Attributes

Key-value strings for arbitrary metadata. Useful for segmentation, preferences, and profile data referenced in rules.

```bash theme={null}
# Set a single attribute
PUT /v1/participants/{id}/state/attributes/region
{"value": "US"}

# Update multiple attributes
PATCH /v1/participants/{id}/state/attributes
{"attributes": {"region": "US", "plan": "premium"}}

# Get an attribute
GET /v1/participants/{id}/state/attributes/region

# Remove an attribute
DELETE /v1/participants/{id}/state/attributes/region
```

### Using attributes in conditions

```javascript theme={null}
participant.attribute.region == "US"
participant.attribute.plan in ["premium", "enterprise"]
```

### Setting attributes from rules

The `SET_ATTRIBUTE` action sets a key-value pair on the event's participant. Unmarked values are always stored as literal strings: `"high"` is stored as the text `high`. For dynamic values, wrap CEL in `${{ ... }}`: `"value": "${{ event.category }}"` stores the evaluated result.

```json theme={null}
{
  "name": "Mark High Spender",
  "condition": "event.type == 'purchase' && event.amount >= 500",
  "actions": [
    {"type": "SET_ATTRIBUTE", "key": "spender_tier", "value": "high"},
    {"type": "SET_ATTRIBUTE", "key": "last_category", "value": "${{ event.category }}"}
  ]
}
```

## Program and Group State

Programs and groups support the same state types as participants. Access them in CEL via `program.*` and `groups[*].*`.

```javascript theme={null}
// Program counter
program.counter.total_issued < 100000

// Program tag
"milestone_reached" in program.tags
```

To update program or group state from a rule, add a `target` to the action:

```json theme={null}
{
  "name": "Limited Giveaway",
  "condition": "event.type == 'claim' && program.counter.total_claims < 1000.0",
  "actions": [
    {"type": "CREDIT", "asset_id": "...", "amount": "50"},
    {"type": "COUNTER", "key": "total_claims", "value": "1", "target": {"type": "PROGRAM"}}
  ]
}
```

## State History

All state changes are logged. Query the history to see what changed, when, and why:

```bash theme={null}
GET /v1/participants/{id}/activity/state-history?state_type=counter&key=lifetime_spend
```

Each entry records the `state_type`, `key`, `operation` (`set` or `delete`), `old_value`, `new_value`, timestamp, and the source of the change. Rule-triggered changes include the `event_id`. Direct API calls include the API key ID.

## State Updates on Inactive Participants

State update behavior depends on participant status:

| State Type | ACTIVE  | SUSPENDED / CLOSED  |
| ---------- | ------- | ------------------- |
| Tags       | Allowed | Allowed             |
| Attributes | Allowed | Allowed             |
| Counters   | Allowed | Blocked (409 error) |
