Test without side effects, then confirm a live run
A workflow that pays members needs two separate answers. First: it does what you think, without paying anyone. Second: it really runs when the trigger fires, and it pays exactly once.The API separates those two steps with one switch: mode. A dry run writes nothing; mode: "live" is the default, so the deliberate act is choosing the dry run, not the live one. Send mode: "dry-run" for every iteration and keep live for the run you mean.The three modes, and what each one proves#
| Mode | What it does | What it proves |
|---|
mode: "dry-run" | Read-only steps run for real; everything that writes, sends or calls is resolved and reported | The logic, the branch taken, the resolved member and amounts |
mode: "live" test (the default) | Every step really runs, like a triggered run | The workflow executes end to end, but leaves no run in the history |
| Triggered run | The workflow runs because its trigger fired | The whole chain: trigger, workflow, run, side effects |
1. Prepare a synthetic member#
Take every field key from the catalog rather than guessing one: GET /catalog returns platformActionFields.get and platformActionFields.post, each entry carrying the key the step stores, the label the platform stores beside it, and whether the field is builtin or custom.Use a member you created for this purpose, and read their current values through the platform's user endpoints. Record the baseline: field values, history rows, and any job counts you will compare.2. Dry-run the workflow#
2.
Every write step is simulated, with wouldDo naming the member, the field, the amount and the logic.
3.
The branch the trace took is the branch you expected.
4.
Any step that is failed names why.
Then prove nothing changed: read the member's value and the workflow's runs again. The counts must be identical to the baseline.3. Prove the negative case#
Send a second dry run with a payload that must not pay: an ineligible member, a zero volume, a missing identifier.The trace must take the other branch, or fail the step for the reason you expect. A workflow that pays everybody is not a workflow you have tested.4. Decide about the live test#
A live test executes for real. It is useful when you need to know the steps run with real data, and it is not a substitute for a real trigger: it leaves no run in the history, so it cannot prove that the trigger is reachable or that the run was recorded.It needs workflowBuilder like every other call, and nothing else: there is no live-test permission and no confirmation header. The mode is the only thing that makes it live, so send it deliberately:Readback: sideEffects: "real", a status, the trace, and a historyId for the transient record the call created. Then read the member's value again: the delta must be exactly one reward.Skip this mode when the workflow pays a live product and a dry run plus a triggered synthetic run already answers the question. The triggered run is the stronger evidence anyway.5. Deploy and fire the real trigger#
Fire the trigger the way a real sender does, with a fresh event id and a synthetic member. Then read, in order:1.
GET /{workflowId}/runs: a new run, with a status.
2.
GET /runs/{runId}: per-step results, including the branch taken.
3.
The member's value: exactly one delta.
4.
For a bulk step, the job's per-row detail.
5.
A replay of the same event: no second delta.
Each link is a separate claim. A run success with an unchanged member value is a failure that looks like a success.6. Something that stays behind#
A dry run writes no history, no step status, no member data and no outbound call. A live test leaves the member's new value: that part is real and must be recorded in your own evidence.7. Failure handling#
| What you see | What it means | What to do |
|---|
403 API_KEY_PERMISSION_DENIED | The key has no workflowBuilder permission | Grant it |
| A live result where you meant a dry run | mode was omitted, and live is the default | Send mode: "dry-run", and check sideEffects in the response: none means nothing was written |
The trace shows a step failed with UNRESOLVED_SIMULATED_REFERENCE | That step reads a step the dry run simulated, and a simulated step produces no output; the workflow status is fail | Test that step directly with input, or dry-run a workflow whose earlier steps are read-only |
Run success, member unchanged | The step was skipped or the amount was zero | Read the run detail and the branch |
| Two deltas for one event | The event reached the workflow twice | Fix deduplication before the next cycle |
| Deploy succeeded with warnings you did not read | Validation is advisory, so no validation finding refuses a deploy | Read warnings from the deploy response, and act on each one before the next cycle |
8. Cleanup#
Undeploy and delete the test workflow when it exists only for this check. Remove the synthetic member through the platform's user endpoints, or keep it deliberately and say so in your record. Delete any copy of a credential that the run produced in a log or artifact.What the API does not cover#
Cancelling a payment that a mistaken live run made. That is your reversal process.
Reading the member's own values. Those are platform endpoints.
A trigger that fires a test event from the platform's side. The sender fires it.
A permission that keeps a key from spending money. One permission covers the whole family, so the guard is the mode you pass and the instructions you give the agent or script holding the key.
Confirming that a member-facing screen shows the reward. That is a separate visual check.
Modified at 2026-10-28 04:44:25