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

# Simulate a draft rule

`POST /v1/rules/simulate`

## Request

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

Dry-run a draft rule (not yet saved) against sample event data. The draft is validated exactly like rule create, then evaluated; nothing is persisted and no ledger changes occur.

## Request body

- SimulateDraftRuleRequest: Simulates an unsaved rule against an event and a participant state. Nothing is saved.
  - `diagnostics` boolean: Include an explanation of each part of the CEL condition. Nothing is saved.
  - `event` object, required: Sample event data to evaluate against the draft rule. Numbers with a magnitude greater than 2^53 are rejected to prevent loss of precision.
  - `event_timestamp` string, date-time: When the sample event occurred. Used to check whether it falls within the rule's active window.
  - `participant_id` string, uuid: A real participant whose current tags, counters, attributes and tiers Scrip loads as the simulation's participant context. Send this or `participant_state`, not both.
  - `participant_state` object: Simulated participant state (tags, counters, attributes), with the same meaning as in `POST /v1/rules/{rule_id}/simulate`. Send this or `participant_id`, not both.
  - `program_id` string, uuid, required: Program providing the validation and evaluation context (asset linkage, tier keys, participant state lookups)
  - `rule` SimulateDraftRule, required: A rule you have not saved, with a condition and actions, to test in a simulation.
    - `actions` RuleAction[], required: Actions to execute when the condition matches (must be non-empty). Same flattened structure as rule create: 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: Optional active window evaluated separately from the CEL outcome.
    - `active_to` string, date-time: The event time the draft rule stops applying, as an RFC 3339 timestamp.
    - `condition` string, required: CEL expression that determines when the rule fires
    - `name` string: Display name echoed back in the response. Defaults to "Draft rule".
    - `stop_after_match` boolean: Whether a successful match would stop later rules in the same proposed set. Single-rule simulation cannot infer neighboring rules, but echoes this for proposed-configuration diagnostic consumers.

## Response `200`

Simulation result

- SimulateRuleResponse: The result of a rule simulation: the rule, the evaluation and any warnings.
  - `evaluation` SimulateEvaluation: The outcome of a simulated rule: whether it matched and the actions it would take.
    - `diagnostics` SimulateConditionDiagnostics: Why a simulated rule matched or did not: the rule's active window, its condition and whether it was reached.
      - `active_window` SimulateDiagnosticActiveWindow: Whether the event fell inside the rule's active dates.
        - `active` boolean: Whether the event time fell inside the rule's active dates.
        - `active_from` string, date-time: The earliest event time the rule applies to.
        - `active_to` string, date-time: The event time the rule stops applying.
        - `event_timestamp` string, date-time: The event time Scrip checked against the dates.
        - `outcome` string: How the active dates affected the result.
      - `cel` ConditionDiagnostic: An explanation of how one rule condition evaluated, clause by clause. Scrip returns it only when you ask for diagnostics and does not store it.
        - `clauses` ConditionDiagnosticClause[]: The condition split into clauses, each listed before its children.
          - `default` ConditionDiagnosticDefault: A default value that Scrip applied to a missing event field while it evaluated a clause.
            - `applied` boolean: Whether Scrip used the default.
            - `note` string: A short explanation of the default.
            - `redacted` boolean: Whether Scrip hid the value because it can contain customer data.
            - `source` string: Where the default came from.
            - `value` unknown
          - `depth` integer: How many clauses deep this clause is. The top clause is 0.
          - `error` ConditionDiagnosticError: An error raised while Scrip evaluated a condition.
            - `kind` string: The kind of error.
            - `message` string: What went wrong.
          - `expression` string: The text of the clause.
          - `id` integer: The number of the clause in this list.
          - `kind` string: What kind of clause this is.
          - `parent_id` integer: The number of the clause that contains this one. The top clause has none.
          - `path` string: The field the clause reads, when it reads one.
          - `redacted` boolean: Whether Scrip hid the value because it can contain customer data.
          - `status` 'EVALUATED' | 'ABSENT' | 'ERROR' | 'SHORT_CIRCUITED' | 'NOT_EVALUATED': How the clause evaluated.
          - `truncated` boolean: Whether Scrip shortened the expression text.
          - `value` unknown
          - `value_type` string: The CEL type of the value, such as `bool` or `null`.
        - `error` ConditionDiagnosticError: An error raised while Scrip evaluated a condition.
          - `kind` string: The kind of error.
          - `message` string: What went wrong.
        - `missing_paths` ConditionDiagnosticMissingPath[]: The fields the condition read that were not there.
          - `handled_by` string: What kept the missing field from failing the condition, such as a default or a check for the field.
          - `kind` string: Whether the field belongs to the event, the participant or the program.
          - `path` string: The field that was missing.
          - `redacted` boolean: Whether Scrip hid details because they can contain customer data.
          - `suggestion` string: How to fix the condition.
        - `result` string: What the condition produced: a boolean, an error, or no value.
        - `short_circuits` ConditionDiagnosticShortCircuit[]: The clauses Scrip skipped because an earlier clause already decided the result.
          - `expression` string: The text of the clause that decided the result.
          - `operator` string: The operator that decided the result early, `&&` or `||`.
          - `reason` string: Why Scrip skipped the clause.
          - `redacted` boolean: Whether Scrip hid details because they can contain customer data.
          - `skipped_expression` string: The text of the clause Scrip did not evaluate.
        - `truncated` boolean: Whether Scrip left out clauses because the condition is large.
        - `value` boolean: The boolean the condition produced. It is absent when the condition raised an error.
      - `final_result` string: The result of the rule: it matched, did not match, or was not evaluated.
      - `reachability` SimulateDiagnosticReachability: Whether an earlier rule stopped evaluation before this rule ran.
        - `reached` boolean: Whether Scrip got to this rule.
        - `reason` string: Why Scrip did or did not get to the rule.
        - `stopped_by_rule_id` string: The earlier rule that stopped evaluation, when one did.
      - `rule_snapshot` SimulateDiagnosticRuleSnapshot: Which version of the rule the simulation used.
        - `rule_history_id` string: The id of the earlier version of the rule, when the simulation used one.
        - `source` string: Whether the simulation used the saved rule, a draft or an earlier version.
      - `state_context` SimulateDiagnosticStateContext: Which participant and program state the simulation read. A replay of an old event reads the current state, not the state at the time.
        - `participant` string: Which participant state the simulation read.
        - `program` string: Which program state the simulation read.
    - `matched` boolean: Whether the condition evaluated to true
    - `reason` string: Error detail when `status` is CONDITION_FAILED
    - `results` object[]: What each action would do, including calculated amounts, counter values, and asset symbols. Present only when matched is true. An explicit target adds resolved_target with external_id, participant_id, program_id, or group_id. Static participant IDs use participant_id. Omitted for the default event participant. Asset actions report from_bucket and/or to_bucket. CREDIT adds value; DEBIT and FORFEIT remove it. HOLD moves AVAILABLE to HELD; RELEASE moves HELD to AVAILABLE. VOID_PENDING returns HELD value to AVAILABLE at its source: the program wallet for PREFUNDED assets, or system issuance for UNLIMITED assets. CREDIT, HOLD, RELEASE, and VOID_PENDING also report reference_id when set. CREDIT reports expires_at and matures_at as full RFC 3339 timestamps, keeping fractional seconds. Durations count from the request time. A CREDIT to AVAILABLE with future matures_at uses DEFERRED. For CREDIT, settles_reference: true means a reference_id was supplied with AVAILABLE (the default). Execution settles pending rewards for that reference, or issues a new credit if none exist. Simulation does not read lots or change the ledger, so it cannot confirm pending rewards or select lots. See https://docs.scrip.dev/api-reference/rules/simulate-a-rule for the result fields by action.
    - `status` 'EVALUATED' | 'CONDITION_FAILED': Whether the condition ran: EVALUATED when it produced a result, CONDITION_FAILED when the CEL expression errored.
  - `rule` SimulateRuleInfo: The rule that a simulation tested.
    - `condition` string: CEL expression that was evaluated
    - `id` string, uuid: Unique identifier for the rule
    - `name` string: The name people see for the rule that was simulated.
    - `order` integer: Evaluation sequence position
    - `stop_after_match` boolean: Whether this rule stops subsequent evaluation in its containing set on match; other sets continue
  - `warnings` RuleWarning[]: Advisories about the simulation context. For example, `missing_participant_context` means the condition reads participant state but the request sent neither `participant_id` nor `participant_state`, so the condition ran against empty defaults.
    - `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 participant not found (code: program_not_found | participant_not_found)
- `409`: The program is archived (code: program_archived)
- `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)
