Skip to main content
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 for qualification, retention, and downgrade policies.

Tags

Boolean flags. A tag either exists on an entity or it doesn’t. Useful for one-time gates, status markers, and segmentation labels.
# 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 normalized to lowercase on write.

Using tags in conditions

// 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:
{
  "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:
{"type": "UNTAG", "tag": "PROMO_ACTIVE"}
To tag or untag a different entity, add a target:
{"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.
# Set a counter
PUT /v1/participants/{id}/state/counters/lifetime_spend
{"value": "500.00"}

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

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

Using counters in conditions

Always use get() for safe access with a default value:
// Simple threshold
get(participant.counters, "purchase_count", 0.0) >= 10.0

// Threshold crossing detection
get(participant.counters, "spend", 0.0) < 1000.0 &&
(get(participant.counters, "spend", 0.0) + 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:
{
  "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").
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. See Writing Rules for details.

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 evaluated lazily on the next read or write. Set reset_after via the API:
PUT /v1/participants/{id}/state/counters/monthly_purchases
{"value": "1", "reset_after": "720h"}
Or from a rule action:
{"type": "COUNTER", "key": "monthly_purchases", "value": "1", "reset_after": "720h"}
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 instead.

Attributes

Key-value strings for arbitrary metadata. Useful for segmentation, preferences, and profile data referenced in rules.
# 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

get(participant.attributes, "region", "") == "US"
get(participant.attributes, "plan", "") in ["premium", "enterprise"]

Setting attributes from rules

The SET_ATTRIBUTE action sets a key-value pair on the event’s participant. The value field supports literal strings and CEL expressions. Plain strings like "high" are stored as-is. Values containing CEL syntax like event.category or string(event.amount) are evaluated as expressions.
{
  "name": "Mark High Spender",
  "condition": "event.type == 'purchase' && event.amount >= 500",
  "actions": [
    {"type": "SET_ATTRIBUTE", "key": "spender_tier", "value": "high"}
  ]
}

Program and Group State

Programs and groups support the same state types as participants. Access them in CEL via program.* and groups[*].*.
// Program counter
get(program.counters, "total_issued", 0.0) < 100000.0

// Program tag
"milestone_reached" in program.tags
To update program or group state from a rule, add a target to the action:
{
  "name": "Limited Giveaway",
  "condition": "event.type == 'claim' && get(program.counters, 'total_claims', 0.0) < 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:
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 TypeACTIVESUSPENDED / CLOSED
TagsAllowedAllowed
AttributesAllowedAllowed
CountersAllowedBlocked (409 error)