Skip to main content

What is a Dry Run?

A Dry Run tests how your rules evaluate a single set of input facts against a specific policy version — without any side effects. No execution logs are written, no integrations are called, and no quotas are consumed. Think of it as a unit test for your rules.
Dry Run vs Impact Simulation: A Dry Run tests one input at a time for quick validation. Impact Simulation tests a version against hundreds or thousands of historical inputs and compares the results against a baseline version. Use Dry Runs for iterative development; use Impact Simulation for regression testing before deployment.

When to Use Dry Run

  • After creating or modifying rules — verify they match expected inputs
  • Before publishing a DRAFT version — your safety net
  • When debugging unexpected execution results — reproduce the scenario
  • When comparing two versions with the same input (Dry Run Compare)

Running a Dry Run

Console

Navigate to a policy version → Dry Run tab → enter input facts → click Run.

CLI

lexq analytics dry-run \
  --version-id <versionId> \
  --debug \
  --mock \
  --json '{"facts": {"payment_amount": 150000, "customer_tier": "VIP"}}'
FlagDescription
--debugInclude execution traces and decision traces in the response
--mockMock external integration calls (webhooks, notifications, etc.)

API

curl -X POST https://api.lexq.io/api/v1/partners/analytics/dry-run/versions/{versionId} \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "facts": { "payment_amount": 150000, "customer_tier": "VIP" },
    "includeDebugInfo": true,
    "mockExternalCalls": true
  }'

Response

{
  "result": "SUCCESS",
  "data": {
    "inputFacts": {
      "payment_amount": 150000,
      "customer_tier": "VIP"
    },
    "mutatedFacts": {
      "payment_amount": 135000
    },
    "generatedVariables": {
      "payment_amount__delta": -15000,
      "discount_applied": true
    },
    "executionTraces": [ ... ],
    "decisionTraces": [ ... ],
    "latencyMs": 12,
    "versionNo": 3
  }
}

State Layers

The data object exposes three layers of state explicitly — never merged into a single blob:
  • inputFacts — facts as received from the request, normalized.
  • mutatedFacts — only the facts whose values were changed by rule actions.
  • generatedVariables — variables created by rule actions that did not exist in the input. This includes per-action change keys like {refVar}__delta (signed change from MUTATE_FACT) and {targetVar}__delta (non-negative increment from INCREMENT_FACT).
The merged “final state” can always be reconstructed as { ...inputFacts, ...mutatedFacts, ...generatedVariables }. Keeping them separate makes audits, replays, and debugging deterministic — you always know which value originated where.

Execution Traces

Each rule that the engine evaluated produces one execution trace. The trace records the exact match expression that was checked, the input facts the rule saw, and the actions it generated:
{
  "tenantId": "acme-corp",
  "policyGroupId": "01f2b274-...",
  "policyVersionId": "a6062090-...",
  "ruleId": "3b16ced1-...",
  "ruleName": "VIP 10% Discount",
  "executedAt": "2026-04-27T09:44:10Z",
  "matched": true,
  "matchExpression": "(customer_tier == 'VIP') && (payment_amount >= 100000)",
  "inputFacts": {
    "customer_tier": "VIP",
    "payment_amount": 150000
  },
  "generatedActions": [
    {
      "type": "MUTATE_FACT",
      "parameters": { "refVar": "payment_amount", "operator": "SUB", "method": "PERCENTAGE", "rate": 10 }
    }
  ]
}
The four identifiers (tenantId, policyGroupId, policyVersionId, ruleId) make every trace self-locating — you never need an external lookup to know which tenant, group, version, and rule produced it.

Decision Traces

While execution traces record what was evaluated, decision traces record what was selected. Each rule appears in decisionTraces with its final status and the reason code that produced it:
{
  "ruleId": "3b16ced1-...",
  "ruleName": "VIP 10% Discount",
  "policyGroupId": "01f2b274-...",
  "policyVersionId": "a6062090-...",
  "status": "SELECTED",
  "reasonCode": "FINAL_WINNER",
  "reasonDetail": null
}
statusMeaning
SELECTEDRule won and its actions were applied
NO_MATCHRule’s condition evaluated to false
NOT_SELECTEDPre-competition filter (e.g., effective date — reserved for future)
BLOCKEDLost mutex or activation group competition (reasonCode specifies scope)
ERRORAction execution failed
reasonCodeWhen it appears
FINAL_WINNERRule was selected as the winner
CONDITION_MISMATCHInput did not satisfy the condition
EFFECTIVE_DATE_INVALIDOutside the rule’s effective date window (reserved)
MUTEX_PRIORITY_LOSTEXCLUSIVE mutex (limit=1) — another rule had higher priority/score
MUTEX_LIMIT_REACHEDMAX_N mutex (limit>1) — mutexLimit already filled by higher-priority rules
GROUP_PRIORITY_LOSTEXCLUSIVE activation group (limit=1) — another rule won by priority
GROUP_LIMIT_REACHEDMAX_N activation group (limit>1) — executionLimit already filled
ACTION_ERRORAction execution raised an error
ENGINE_ERROREngine-internal error during evaluation
reasonDetail is nullable — it carries a human-readable message when the engine has additional context to surface (e.g., the specific limit value that was hit).
See Decision Trace in the Policy Rules reference for the full status × reasonCode mapping.

Checking Requirements

Before running a Dry Run, check which facts the version expects:
lexq analytics requirements --group-id <gid> --version-id <vid>

Dry Run Compare

Compare how two versions evaluate the same input facts, side by side:
lexq analytics dry-run-compare --json '{
  "versionIdA": "<baseline-version>",
  "versionIdB": "<candidate-version>",
  "facts": { "payment_amount": 200000, "customer_tier": "GOLD" }
}'

Next Steps

Impact Simulation

Test against hundreds of historical inputs at once.

Deployments

Deploy your validated version to production.