---
title: "Create a rule"
method: POST
path: "/v1/rules"
tags: ["Rules"]
---

# Create a rule

`POST /v1/rules`

## Request

- Base URL: `https://api.scrip.dev`
- URL: `https://api.scrip.dev/v1/rules`
- Auth: HTTP bearer

Create a rule that evaluates a CEL condition against incoming events and executes actions when matched. Omit rule_set_id to use the default set. Omit position to append, or use first/last/before/after placement. ACTIVE and SUSPENDED rules retain unique positions 1..N. The response includes the resulting rule_configuration_version.

## Headers

- `Idempotency-Key` string

## Request body

- CreateRuleRequest: Request body for creating a new rule with CEL condition and actions
  - `actions` RuleAction[], required: Actions to execute when the condition matches (must be non-empty). Each action has a `type` field that determines which other fields are relevant.
    - union: A single action to execute when a rule matches. Actions are flat objects: every field sits at the top level alongside `type`, and `type` selects which fields are allowed. Fields that do not appear in the selected branch are rejected.
      - RuleActionCredit (unresolved $ref)
      - RuleActionDebit (unresolved $ref)
      - RuleActionHold: Move value from `AVAILABLE` to `HELD`. Use `reference_id` to link the hold to a later `RELEASE`; this requires a LOT-mode asset.
        - `amount` string, required: Use a positive numeric string, such as "100", or a ${{ }} CEL expression that returns a positive number, such as "${{ event.amount * 0.03 }}". A static amount of zero or less is rejected when you save the rule. An expression must return a positive amount when the action runs.
        - `asset_id` string, uuid, required: Asset to operate on
        - `bucket` 'AVAILABLE' | 'HELD': The balance bucket the action targets. Defaults to AVAILABLE, or HELD for RELEASE.
        - `description` string: Context string recorded on the ledger entry
        - `reference_id` string: ID that links a hold to its release, or pending rewards to their settlement. Requires a LOT-mode asset. Use a literal such as "auth_12345" or a ${{ }} CEL expression such as "${{ event.authorization_id }}".
        - `type` 'HOLD', required: Action kind. Selects which other fields this action accepts.
      - union: Release held funds back to the available bucket. Provide amount for a partial release, or reference_id to release the correlated hold. At least one of the two is required.
        - object
          - `amount` string, required: Use a positive numeric string, such as "100", or a ${{ }} CEL expression that returns a positive number, such as "${{ event.amount * 0.03 }}". A static amount of zero or less is rejected when you save the rule. An expression must return a positive amount when the action runs.
          - `asset_id` string, uuid, required: Asset to operate on
          - `bucket` 'AVAILABLE' | 'HELD': The balance bucket the action targets. Defaults to AVAILABLE, or HELD for RELEASE.
          - `description` string: Context string recorded on the ledger entry
          - `reference_id` string: ID that links a hold to its release, or pending rewards to their settlement. Requires a LOT-mode asset. Use a literal such as "auth_12345" or a ${{ }} CEL expression such as "${{ event.authorization_id }}".
          - `type` 'RELEASE', required: Action kind. Selects which other fields this action accepts.
        - object
          - `amount` string: Use a positive numeric string, such as "100", or a ${{ }} CEL expression that returns a positive number, such as "${{ event.amount * 0.03 }}". A static amount of zero or less is rejected when you save the rule. An expression must return a positive amount when the action runs.
          - `asset_id` string, uuid, required: Asset to operate on
          - `bucket` 'AVAILABLE' | 'HELD': The balance bucket the action targets. Defaults to AVAILABLE, or HELD for RELEASE.
          - `description` string: Context string recorded on the ledger entry
          - `reference_id` string, required: ID that links a hold to its release, or pending rewards to their settlement. Requires a LOT-mode asset. Use a literal such as "auth_12345" or a ${{ }} CEL expression such as "${{ event.authorization_id }}".
          - `type` 'RELEASE', required: Action kind. Selects which other fields this action accepts.
      - RuleActionForfeit: Forfeit an amount out of the participant's balance. Requires asset_id and a positive amount. reference_id is not supported.
        - `amount` string, required: Use a positive numeric string, such as "100", or a ${{ }} CEL expression that returns a positive number, such as "${{ event.amount * 0.03 }}". A static amount of zero or less is rejected when you save the rule. An expression must return a positive amount when the action runs.
        - `asset_id` string, uuid, required: Asset to operate on
        - `bucket` 'AVAILABLE' | 'HELD': The balance bucket the action targets. Defaults to AVAILABLE, or HELD for RELEASE.
        - `description` string: Context string recorded on the ledger entry
        - `type` 'FORFEIT', required: Action kind. Selects which other fields this action accepts.
      - RuleActionVoidPending: Cancel pending rewards credited into `HELD` for `reference_id` and return the value to its source. Requires `reference_id` and a LOT-mode asset. Omit `amount` to cancel all pending rewards, or set a positive amount in asset units to cancel part, newest lots first. An amount above the pending balance cancels only what remains. A blank `amount` is rejected. Do not send `bucket`.
        - `amount` string: Use a positive numeric string, such as "100", or a ${{ }} CEL expression that returns a positive number, such as "${{ event.amount * 0.03 }}". A static amount of zero or less is rejected when you save the rule. An expression must return a positive amount when the action runs.
        - `asset_id` string, uuid, required: Asset to operate on
        - `description` string: Context string recorded on the ledger entry
        - `reference_id` string, required: ID that links a hold to its release, or pending rewards to their settlement. Requires a LOT-mode asset. Use a literal such as "auth_12345" or a ${{ }} CEL expression such as "${{ event.authorization_id }}".
        - `type` 'VOID_PENDING', required: Action kind. Selects which other fields this action accepts.
      - RuleActionTag (unresolved $ref)
      - RuleActionUntag (unresolved $ref)
      - RuleActionCounter (unresolved $ref)
      - RuleActionSetAttribute (unresolved $ref)
      - RuleActionSetTier (unresolved $ref)
      - RuleActionScheduleEvent (unresolved $ref)
      - RuleActionBroadcast: Send an event to every participant in the program. Delivery starts without a delay and runs asynchronously. Do not send `target`; the broadcast already reaches everyone. Use `SCHEDULE_EVENT` for a delayed event.
        - `event_data` object: Event data submitted when the action fires. String values may embed ${{ }} CEL expressions (at any nesting depth, values only), resolved against the triggering rule's context when the action executes.
        - `type` 'BROADCAST', required: Action kind. Selects which other fields this action accepts.
  - `active_from` string, date-time: Start of the rule's active window (RFC 3339). Null means immediately active. Checked against the event's event_timestamp, inclusive.
  - `active_to` string, date-time: End of the rule's active window (RFC 3339). Null means no end date. Checked against the event's event_timestamp, exclusive.
  - `budgets` BudgetSpec[]: Optional budget constraints for this rule. Omit or pass null for no budgets.
    - union: Limit on how much of one asset this rule can credit. Use PROGRAM for a shared limit or PARTICIPANT for a limit per recipient. One budget is allowed per asset and scope. Omit schedule_type for a lifetime limit, or choose CRON or INTERVAL to reset it on a schedule.
      - object: Never resets. schedule_type, cron_expression, and interval are all rejected.
        - `asset_id` string, uuid, required: Asset this budget constrains
        - `limit` string, required: Maximum allowed spend per budget period, as a positive decimal string (e.g. "10000.00"). Budgets are static: ${{ }} expressions are not supported here.
        - `scope` 'PROGRAM' | 'PARTICIPANT': Whether the limit caps all credits of this asset from the rule (PROGRAM, the default) or each recipient's credits (PARTICIPANT). A referral credit counts against the referrer. Credits to GROUP or PROGRAM targets do not count toward a PARTICIPANT budget. A PARTICIPANT budget requires a CREDIT of this asset to a participant in the same rule.
      - object: Resets on a UTC cron schedule. For PARTICIPANT budgets, every participant resets at the same time. Requires cron_expression without a timezone. Do not send interval.
        - `asset_id` string, uuid, required: Asset this budget constrains
        - `cron_expression` string, required: Cron expression for CRON-based resets, evaluated in UTC (e.g. "0 0 1 * *" for first of every month)
        - `limit` string, required: Maximum allowed spend per budget period, as a positive decimal string (e.g. "10000.00"). Budgets are static: ${{ }} expressions are not supported here.
        - `schedule_type` 'CRON', required: Reset schedule type: CRON or INTERVAL. Omit for lifetime (no-reset) budgets.
        - `scope` 'PROGRAM' | 'PARTICIPANT': Whether the limit caps all credits of this asset from the rule (PROGRAM, the default) or each recipient's credits (PARTICIPANT). A referral credit counts against the referrer. Credits to GROUP or PROGRAM targets do not count toward a PARTICIPANT budget. A PARTICIPANT budget requires a CREDIT of this asset to a participant in the same rule.
      - object: Resets after each interval. For PARTICIPANT budgets, each participant's period starts with their first credit from this rule. Requires interval. Do not send cron_expression.
        - `asset_id` string, uuid, required: Asset this budget constrains
        - `interval` string, required: Duration for INTERVAL-based resets; w, d, h, m or s units (e.g. "30d", "720h")
        - `limit` string, required: Maximum allowed spend per budget period, as a positive decimal string (e.g. "10000.00"). Budgets are static: ${{ }} expressions are not supported here.
        - `schedule_type` 'INTERVAL', required: Reset schedule type: CRON or INTERVAL. Omit for lifetime (no-reset) budgets.
        - `scope` 'PROGRAM' | 'PARTICIPANT': Whether the limit caps all credits of this asset from the rule (PROGRAM, the default) or each recipient's credits (PARTICIPANT). A referral credit counts against the referrer. Credits to GROUP or PROGRAM targets do not count toward a PARTICIPANT budget. A PARTICIPANT budget requires a CREDIT of this asset to a participant in the same rule.
  - `condition` string, required: CEL expression that determines when the rule fires
  - `description` string: Human-readable summary of what this rule does
  - `name` string, required: The name people see for the rule.
  - `position` SemanticPositionRequest: Where to place a resource: first, last, or before or after another resource.
    - `placement` 'before' | 'after', required: Placement within the destination collection.
    - `reference_id` string, uuid, required: Stable sibling ID. Required for before/after and omitted for first/last.
  - `program_id` string, uuid, required: Program to attach this rule to
  - `rule_set_id` string, uuid: Rule set to attach this rule to. Omit to use the program's default set.
  - `status` 'ACTIVE' | 'SUSPENDED': Initial lifecycle status (ACTIVE or SUSPENDED). Defaults to ACTIVE.
  - `stop_after_match` boolean: When true, no subsequent rules in this set evaluate after this one matches; other sets continue

## Response `201`

Rule created

- RuleResponse: A rule: a condition and the actions Scrip takes when an event matches it.
  - `actions` object[]: Actions to execute when the condition matches
  - `active_from` string, date-time: Start of the rule's active window (RFC 3339, null if always active). Checked against the event's event_timestamp, inclusive.
  - `active_to` string, date-time: End of the rule's active window (RFC 3339, null if no end date). Checked against the event's event_timestamp, exclusive.
  - `budgets` BudgetResponse[]: Budget constraints applied to this rule
    - `asset_id` string, uuid: Asset this budget constrains
    - `consumed` string: Amount credited in the current budget period. Returned only for PROGRAM budgets; PARTICIPANT usage differs for each recipient.
    - `cron_expression` string: Cron expression for CRON-based resets, evaluated in UTC (e.g., first of every month)
    - `interval` string: Duration for INTERVAL-based resets
    - `limit` string: Maximum allowed spend for this budget period (per participant for a PARTICIPANT budget)
    - `next_reset_at` string, date-time: Next scheduled reset. Omitted for lifetime budgets and PARTICIPANT budgets.
    - `schedule_type` 'CRON' | 'INTERVAL': How the budget resets. Omitted for lifetime budgets.
    - `scope` 'PROGRAM' | 'PARTICIPANT': Whether the limit caps all credits of this asset from the rule (PROGRAM) or each recipient's credits (PARTICIPANT).
  - `condition` string: CEL expression that determines when the rule fires
  - `created_at` string, date-time: When this rule was created (RFC 3339)
  - `deleted_at` string, date-time: When this rule was archived (null if not archived)
  - `description` string: Human-readable summary of what this rule does
  - `id` string, uuid: Unique identifier for this rule
  - `name` string: The name people see for the rule.
  - `order` integer: Evaluation position within the containing rule set (lower = first; ACTIVE and SUSPENDED rules occupy unique positions)
  - `program_id` string, uuid: Program this rule belongs to
  - `rule_configuration_version` integer: Resulting program rule-configuration revision. Present on mutation responses.
  - `rule_set_id` string, uuid: Rule set that contains this rule and scopes its order and stop_after_match behavior
  - `status` 'ACTIVE' | 'SUSPENDED' | 'ARCHIVED': The rule's lifecycle state.
  - `stop_after_match` boolean: When true, no subsequent rules in this set evaluate after this one matches; other sets continue
  - `updated_at` string, date-time: When this rule was last modified (RFC 3339)
  - `warnings` RuleWarning[]: Non-blocking advisories about the rule's condition or action expressions, such as unknown state-key typos or deprecated CEL aliases. Present on create/update only; never blocks the save.
    - `client_ref` string: The `client_ref` of the rule, when the rule is new.
    - `code` string: What the warning is about. On a rule: `unknown_state_key` (a key no rule writes), `deprecated_alias`, or `possible_unmarked_expression` (a literal that looks like CEL without `${{ }}`). On a simulation: `missing_participant_context`. On a configuration preview, also `stop_after_match_has_no_following_rule`, `unconditional_stop_shadows_following_rules`, or `later_rule_set_retained_empty`.
    - `key` string: The referenced key, as written in the condition.
    - `kind` string: Kind of reference: `counter`, `tag`, `attribute`, or `alias`.
    - `message` string: Human-readable explanation (includes the suggestion when one exists).
    - `rule_id` string: Rule identity is populated for complete-configuration validation. The single-rule authoring surfaces omit these fields.
    - `rule_set_id` string: The id of the rule set that holds the rule.
    - `scope` string: Where the referenced state belongs: `participant`, `program`, or `group`.
    - `suggestion` string: Closest known key, when a near-match exists. Empty otherwise.

## Other responses

- `400`: Invalid request, malformed CEL, or invalid action (code: validation_error | invalid_json)
- `401`: The API key is missing or not valid. See [Authentication](https://docs.scrip.dev/authentication).
- `403`: The caller is not allowed to do this.
- `404`: Program or rule set not found (code: program_not_found | rule_set_not_found)
- `409`: Another request placed a rule at the same time, or the program is archived (code: rule_order_conflict | program_archived)
- `422`: The rule set belongs to a different program, or position.reference_id is not in the rule set (code: rule_set_program_mismatch | invalid_position)
- `429`: Too many requests. Wait the number of seconds in the `Retry-After` header, then retry. See [Limits and durations](https://docs.scrip.dev/limits#rate-limits).
- `500`: Something failed on Scrip's side. Retry the request, and quote `request_id` if it keeps failing. [Errors](https://docs.scrip.dev/errors) lists every code, and the [API introduction](https://docs.scrip.dev/api-reference/introduction) describes the error format.

---

[API](https://skmtc.dev/scrip/apis/scrip-api.md) · [All operations](https://skmtc.dev/scrip/apis/scrip-api/llms.txt) · [OpenAPI document](https://skmtc.dev/scrip/apis/scrip-api/revisions/c52d35da8d97?raw)
