Skip to content

Debugging Rules

Debug from the first point where the data stops matching what you expect: request, then workflow, then rule, then action, then wallet. The workflow record (GET /projects/{projectId}/workflows/{eventId}) answers most questions. Do not re-ingest an event to “see if it works now”. That creates a second workflow and can award twice.

Gather the project, the event id (evt_…, which is also the workflow id), userId, the rule id, the exact request body, the expected result and the observed result.

SymptomCheckTypical cause and fix
Ingest returns 400The error array: each issue has path and messagepayload missing, type empty, or userId not a string (123 → "123").
Ingest 401 (code: 1002)HeaderSend the key in X-API-Key or Authorization: Bearer <key>.
Ingest 201 but a field “isn’t there”triggerData in the workflowUnknown top-level fields are silently dropped. Move amount, orderId and similar fields into payload.
Workflow stays 404Poll longer, then GET /workflows?eventId=evt_…Processing is queued. Missing after a long wait: report it with the event id instead of re-sending.
Rule absent from rules[]GET /projects/{projectId}/rules/{ruleId}: isActive, validFrom/validTo, event resource key vs event typeInactive, out of window, or bound to another type (purchase vs order_completed, case-sensitive).
Rule in rules[] with matched: falseRerun its conditions in POST /rules/test with {"event": triggerData} plus any resource values from resolvedContextLook at results[].resolvedLeft. undefined means a wrong path ($('event').amount → $('event').payload.amount) or an undeclared resource.
Condition reads undefined for a resourceRule resources[] and resolvedContextThe resource is not declared on the rule (it is silently undefined) or not registered on the project. Add it to the rule’s resources.
Resource failedresources[] entry with status: "failed" and errorFix the data source (URL, auth, table, script). Dependent conditions evaluated false.
Rule fires on unrelated eventsRule resources[]The rule has no event-type binding (generic event key and no $('event').type condition), so it matches every type.
Action failed: Reward action requires userIdtriggerData.userId, resolvedUserIdThe event was sent without userId, or the userId expression resolved to nothing.
Action failed: … amount must be a positive numberactions[].payload.amountThe amount expression came out negative.
Action completed with result.skipped: trueactions[].payload.amountThe expression came out as 0 (for example Math.floor(amount / 100) on 49 cents).
Action failed: HTTP 4xx/5xx or Integration call failedactions[].payload (resolved body) and result.bodyReceiver rejected it, timeout, or wrong URL or auth. See HTTP integrations.
Workflow failedTop-level error. actions is emptyAn action threw, for example Insufficient balance for asset 'points' on a redemption. Earlier actions may have applied, so check the wallet.
Reward 100× too big or never matchesPayload amount vs thresholdsUnits mix-up. Send integer cents (7450), compare in cents (5000), divide by 100 for points per dollar.
Reward applied twiceTwo workflows for the same payload.orderIdThe event was ingested twice. Ingest has no idempotency key, so deduplicate at the source.
Literal {{ … }} text in a description or webhook bodyConfig valueOnly a value that is entirely one {{ … }} expression is evaluated. Use "{{ 'Order ' + $('event').payload.orderId }}".
POST /projects/{projectId}/rules/test
{
"conditions": { "combinator": "AND", "conditions": [
{ "leftValue": "{{ $('event').payload.amount }}", "operator": "gte", "rightValue": 5000, "rightType": "number" },
{ "leftValue": "{{ $('wallet').balances.points }}", "operator": "lt", "rightValue": 10000, "rightType": "number" }
]},
"context": {
"event": { "type": "purchase", "userId": "cust_8421", "payload": { "amount": 7450 } },
"wallet": { "balances": { "points": 2300 } }
}
}

/rules/test resolves nothing on its own. You supply every resource value. That is why it is the right tool for “is the expression right?”, while the workflow’s resolvedContext answers “what data did the live rule see?”.

POST /projects/{projectId}/rules/evaluate runs all active rules against a context but does not apply the live event-type binding. A rule bound only by its event resource key can match there and still not run live. Treat /rules/test plus the workflow record as the source of truth.

  • POST /projects/{projectId}/rules/execute-actions and /rules/execute run real actions. They write to wallets and call integrations. Use a test user or test project.
  • To check a balance or ledger, read GET /projects/{projectId}/users/{userId}/wallet and .../wallet/transactions. Do not call earn or spend to “probe”.

Include the redacted request, the evt_ id, the rule id, the relevant workflow excerpt (rules, actions, resources, error), and the expected and observed wallet state. Never include API keys or integration secrets.

For promotions (cart discounts), debug with POST /projects/{projectId}/promotions/evaluate instead. See Creating promotions and Discount scripts.

Workflows · Conditions and resources · Loyalty events · Sending business events