Bind step inputs to trigger and earlier-step outputs
Most workflow failures that look like "the wrong value was written" are binding failures: a step reads a literal string where it needed a reference, or a reference that resolves to nothing. The API makes bindings visible, so a binding is something you can write, read back and dry-run before it runs.
The core rule
A value slot in step options is a list. A bare string in that list is text. A reference is either a template string or a reference object:
- a template:
"{{<nodeId>.<field>}}", where<nodeId>is a 24-character step id or the trigger's_id - an object:
{"nodeId": "<stepId>", "field": "root.email", "defaultValue": "<safe-value>"}
A bare dotted string such as "webhook_trigger.email" is not a binding. It is text, and it is what a step writes when the binding is wrong. Do not save one.
The trigger's id is trigger._id, and a trigger payload field is normally read from root.
A reference into a read step uses the field's label, because that is how the step names its result. Take the label from platformActionFields in GET /catalog, for example Total XP, and reference {{<readStepId>.Total XP}}. A label you send is kept as sent, so the label in your node is the one later steps must use; a community that renamed its XP unit returns a different catalog label for the same key. Neither mistake is refused. A field key that is not one of the community's fields is stored as sent, and a reference to a label the step does not produce is saved too. POST /{workflowId}/validate reports them as the warnings UNKNOWN_USER_FIELD (with the allowed keys) and REFERENCE_UNKNOWN_FIELD, and deploy still proceeds. At run time the step reads or writes nothing: a write step with a mistyped key pays nothing and reports no error. Read the warnings before you deploy.
1. Read the field keys, then the two ids you need
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.
curl --request GET \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b" \
--header "Authorization: Bearer <API_KEY>"
From the response take trigger._id (the source for trigger fields) and the _id of the step whose output you want to read. Never copy an id from another workflow: ids are per workflow even when the step names and payloads look identical.
2. Know how a reference path is read
The run path reads a reference with its own rules, and validation applies the same ones. Validation is advisory: every finding below is a warning in POST /{workflowId}/validate and in the deploy response, the write and the deploy both succeed, and the reference resolves to nothing at run time. Read the warnings before you deploy:
- A reference object's
fieldis read from the source step's result. The value offieldis the step id itself, meaning "the whole result"; anything else is a path into it. - A
{{nodeId.path}}template becomespathon the source step's result. A template whose first part is a 24-hex step id that is not in the workflow is reported as the warningREFERENCE_UNRESOLVABLE, and a run leaves it as literal text. A{{...}}whose first part is not an id at all, such as{{amount}}, is reported as the warningTEMPLATE_NOT_A_REFERENCE, because a run leaves that text as it is and the value never resolves. The id must also sit directly against the dot:{{<id> .path}}is not a reference. A copy is not the case to worry about: duplicate and import rewrite the templates they carry. - A path that names a label the step does not produce is
REFERENCE_UNKNOWN_FIELD; a path that reads a field off a number or a list isREFERENCE_SHAPE_MISMATCH. - A step whose label is empty cannot be referenced:
POST /{workflowId}/validatereportsEMPTY_FIELD_LABELwhen another step reads it, and the deploy still proceeds, so the reader resolves to nothing at run time.
3. Write the pair together
A reward step needs two independent bindings: the identity, and the value. Fixing only one is the most common half-fix.
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
--data '{"type":"platform-action","name":"Add volume XP","position":{"after":"65f1a2b3c4d5e6f708192a3c"},"options":{"action":"post-user-fields","selectUser":{"type":"email","value":[{"nodeId":"65f1a2b3c4d5e6f708192a41","field":"root.email","defaultValue":"trader@example.com"}]},"fields":[{"label":"XP","field":"xp","logic":"add","value":[{"nodeId":"65f1a2b3c4d5e6f708192a3c","field":"volume_xp","defaultValue":"0"}]}]}}'
The selector reads the trigger; the amount reads an earlier step. Both are references, so both resolve at run time.
The template form is shorter and equivalent:
curl --request PATCH \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes/65f1a2b3c4d5e6f708192a3d" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--header "If-Match: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501" \
--data '{"options":{"action":"post-user-fields","selectUser":{"type":"email","value":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"fields":[{"label":"XP","field":"xp","logic":"add","value":["{{65f1a2b3c4d5e6f708192a3c.volume_xp}}"]}]}}'
4. Keep the default harmless
defaultValue is a test value, and the API's dry run does not use it. A dry run resolves a value exactly as a triggered run would, so a missing path stays missing. A live test uses the canvas test resolution, where a default stands in for a missing value, and a live test that pays really writes it. A triggered run never uses a default: it fails the step with Missing workflow mapping: source node ..., field ....
- Keep defaults harmless anyway: a synthetic identifier, zero, or an empty CSV. A live test will use them.
- Never use a real customer id or a reward amount as a default.
- A test seeded with
triggerPayloaddoes not need a default at all, and a triggered run ignores it.
5. Prove the binding before it pays
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{"mode":"dry-run","triggerPayload":{"email":"trader@example.com","volume_xp":"250"}}'
Read the trace entry for the step you changed:
wouldDo.selectUser.valuemust be the member identifier, not{{...}}text and notroot.email.wouldDo.fields[0].valuemust be the amount, not the field expression.wouldDo.usersmust contain the member the platform resolved. An empty list means the selector did not resolve.
Then read the step back with GET /{workflowId}/nodes/{nodeId}: the stored options must show the reference, not the resolved sample. A stored sample that happens to look right is the failure this check exists to catch.
6. When a value does not resolve
| What the dry run shows | Likely cause | Fix |
|---|---|---|
wouldDo.users empty, step failed with User not found | The identity is text, or the field path is wrong for this trigger | Fix the selector's nodeId and field. |
| The written value is the mapping expression | The value slot holds a bare string | Replace it with a template or a reference object. |
REFERENCE_NOT_UPSTREAM from validate | The step reads a step that runs after it, or a sibling branch | Move the step, or read an earlier step. |
REFERENCE_MAY_BE_UNSET warning | The step reads a branch step that may not have run | Accept it, or move the read into the same branch. |
REFERENCE_UNRESOLVED | The id belongs to another workflow | Use this workflow's own step id. |
REFERENCE_UNKNOWN_FIELD | The reference names a label the step does not produce | Use the label on that step's fields[], or the catalog's label for the key. |
REFERENCE_SHAPE_MISMATCH | The reference reads a field off a number or a list | Read the value whole, or read a field of an object result. |
REFERENCE_UNRESOLVABLE | The step id in the reference or template is not in this workflow | Use this workflow's own step id. |
TEMPLATE_NOT_A_REFERENCE warning | A {{...}} whose first part is not a step id, so the run leaves it as text | Reference a step id, or keep the text deliberately. |
| A reference object resolves to nothing | The path is correct but the payload has no such field: a triggered run fails the step with Missing workflow mapping | Fix the sender's payload or the path. A defaultValue does not cover a triggered run. |
| A template resolves to nothing | A {{nodeId.path}} whose path is missing from the payload becomes an empty string, and the run continues with it. A template whose node id is not in the workflow stays as literal text | Validate before deploying: REFERENCE_UNRESOLVABLE reports a template that names no step in the workflow, and REFERENCE_UNKNOWN_FIELD reports a path its step does not produce. Neither is visible at run time. |
7. After deployment
A binding that resolves in a dry run can still meet a payload it did not expect. Read the member's own field value after the first real run: a run result proves the step ran, and the member's value proves it wrote what you meant.
What the API does not cover
- Resolving a bare
node.fieldstring for you. It is text by design; the API does not guess. - Copying a reference from another workflow. Ids are per workflow, so a copied reference is unresolved and validation reports it.
- Choosing a field that does not exist on the member. That is a platform field question, not a workflow one.
Endpoints on this page
- Get a workflow:
GET /v1/workflow-builder/data-workflows/{workflowId} - Add a step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes - Update a step:
PATCH /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId} - Test a whole workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/test - List step types and trigger types:
GET /v1/workflow-builder/data-workflows/catalog - Validate a stored workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/validate - Get a step:
GET /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId}