Returning.AIDocs
v1

API reference / Workflow Builder

.md

Add a step

Adds one step to a workflow: at a position on the main line, or inside a router branch.

Last updated 28 Oct 2026API v1

Method
POST
Path
https://api.returning.ai/v1/workflow-builder/data-workflows/{workflowId}/nodes
Permission
workflowBuilder
Retries
Adding a step is not idempotent: two accepted calls create two steps.

Authentication#

Header
Authorization: Bearer <API_KEY>
Permission
workflowBuilder

Authorization: Bearer <API_KEY> with workflowBuilder, on a workflow of the key's community. If-Match is optional. Send the revision you read to avoid overwriting a concurrent change (a stale value is refused with 409 VERSION_CONFLICT); omit it and your write wins as the last one. A deployed workflow is edited with the same call: the change applies immediately.

FailureMeaningRecovery
400 VALIDATION_FAILEDThe options do not match the step type, or position names a step that is not on the main lineRead the type's schema from GET /catalog, or read the workflow for current step ids.
404 BRANCH_NOT_FOUNDposition.branchIndex is out of range for that routerRead the router and use an existing branch index.

Behaviour#

The step's options are validated against its type before anything is stored, and the position is resolved by the server.

A branch step is registered with its branch in the same call. The response carries the stored step and the workflow's new revision.

Request#

Path parameters#

workflowId#stringREQUIRED
Workflow identifier: 24 hexadecimal characters, as returned in _id.

Rule^[0-9a-fA-F]{24}$

Eg65f1a2b3c4d5e6f708192a3b

Headers#

Authorization#stringREQUIRED
Community API key: Bearer <API_KEY>.

EgBearer <API_KEY>

If-Match#stringOPTIONAL
Optional. Send the revision the change is based on, bare or quoted, to avoid overwriting a concurrent change: a stale value is refused with 409 VERSION_CONFLICT. Omit it for last-write-wins.

Eg162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c

Content-Type#stringREQUIRED
Request body format.

Ruleapplication/json

Body#

type#stringREQUIRED
Step type.

Rulehttp, platform-action, math, send-email, send-emails, code, router, users-bulk-update

Eg"platform-action"

name#stringREQUIRED
Step name shown on the canvas.

RuleMax 200 chars

Eg"Read member XP"

position#objectOPTIONAL
Where the step goes. Omit to append to the main line.

Eg{"after": "trigger"}

after#stringOPTIONAL
Insert after this step id, or after trigger for the first position.

Eg"trigger"

routerNodeId#stringOPTIONAL
Router step id, when the step belongs inside a branch.

Eg"65f1a2b3c4d5e6f708192a3e"

branchIndex#integerOPTIONAL
Branch index inside that router.

RuleMin 0

Eg0

options#objectOPTIONAL
Step options, shaped by the step type's schema from GET /catalog.

Eg{ ... }

continueOnFailure#booleanOPTIONAL
Keep going when this step fails. Defaults to false.

Egfalse

retryOnFailure#booleanOPTIONAL
Retry this step when it fails. Defaults to false.

Egfalse

ref#stringOPTIONAL
Only for a node list posted in one call: a local name other steps can reference. Defaults to node-<index>.

RuleMax 120 chars

Eg"read-member"

Watch for

  • position.after: "trigger" inserts before the current first step. Omitting position appends to the end of the main line.
  • A branch step is created on the main line first and then registered with its branch, so a step that is in a branch still appears in the workflow's id list.
  • options must be the complete payload for the type. A partial options object is not merged with anything: there is nothing stored yet.
  • A platform-action field key must be a real field of the community. A read action accepts total_xp, total_coins, xp and coin; a write action accepts xp and coin; a custom field uses its own key. GET /catalog lists them. A key outside the list is stored as sent, and POST /{workflowId}/validate reports UNKNOWN_USER_FIELD as a warning, so read the list instead of guessing: a step that reads nothing fails at run time, not at write time.
  • A label you send is stored exactly as sent. Only a missing or empty label is filled from the catalog, for example Total XP for a read of total_xp and XP for a write to xp, so send the catalog's label unless you need a specific one.
  • A read step's result is keyed by that label, so a later step references {{<readStepId>.Total XP}}, not the field key. Validation reports REFERENCE_UNKNOWN_FIELD when a reference names a label the step does not produce.
  • An HTTP step's auth must be present. Use {"type":"bearer-token","token":[]} for "no auth"; an empty value is not treated as a secret.
  • A credential-named header or parameter is redacted on read even when you authored it as a template. See redactedPaths in the response.
  • A Code step is a normal step type here: type: "code" with options.inputs and options.sourceCode.code. It can be created, changed, duplicated, imported and tested like any other step; sideEffects: true because its source can write.
  • A leaderboard step reads that leaderboard's own community, so it may only name a leaderboard of the key's community.
  • continueOnFailure and retryOnFailure default to false. Set them explicitly when the workflow must continue past a failure.
  • Send logic explicitly on every write step's field (add, subtract, overwrite). The stored default is overwrite, which can zero a balance, and nothing refuses the write: POST /{workflowId}/validate reports POST_LOGIC_REQUIRED as a warning.
  • A step id should appear once across a router's branch lists: a second listing makes a run execute the step twice. Nothing refuses the write; POST /{workflowId}/validate reports NODE_LISTED_TWICE as a warning.
  • retryOnFailure is for a step that can fail and be repeated safely: one HTTP call, one email, one bulk update, or a write of one field. POST /{workflowId}/validate warns with RETRY_REPEATS_SIDE_EFFECTS on a router whose branches contain a step that pays or calls outside, and on a write step that sets more than one field, because a retry would repeat several side effects at once. Nothing refuses the write.
More examples and details

Append an HTTP step after an existing step:

BASH
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":"http","name":"Notify receiver","position":{"after":"65f1a2b3c4d5e6f708192a3c"},"options":{"method":"POST","url":["https://receiver.example.com/trade-bonus"],"params":{},"headers":{"X-Api-Key":["<RECEIVER_KEY>"]},"bodyType":"json","body":{"member":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"auth":{"type":"bearer-token","token":[]}}}'

A router with one condition branch and one fallback. Conditions use AND groups: OR and WHERE groups are stored and deployed, and the runtime then treats the group as always true, so validation warns ROUTER_OPERATOR_UNSAFE:

BASH
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: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501" \
  --data '{"type":"router","name":"XP over 100?","position":{"after":"65f1a2b3c4d5e6f708192a3c"},"options":{"executePaths":"first_matching","branches":[{"branchName":"XP > 100","branchType":"CONDITION","conditions":[{"id":"group-1","operator":"AND","nestedGroups":[{"id":"nested-1","operator":"AND","conditions":[{"id":"condition-1","key":["{{65f1a2b3c4d5e6f708192a3c.Total XP}}"],"type":"number","operator":"greater_than","value":["100"]}]}]}]},{"branchName":"Otherwise","branchType":"FALLBACK","conditions":[]}]}}'

A step inside the router's first branch. The branch index is the position in branches, and the step is also added to the workflow's step order automatically:

BASH
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: 9ab88dc5d9fdaada545d24e2d76d58cd758c7b99eb905df867d90cdda294e12f" \
  --data '{"type":"platform-action","name":"Add 10 XP","position":{"routerNodeId":"65f1a2b3c4d5e6f708192a3e","branchIndex":0},"options":{"action":"post-user-fields","selectUser":{"type":"email","value":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"fields":[{"label":"XP","field":"xp","logic":"add","value":["10"]}]}}'
JSON
{
  "data": {
    "node": {
      "_id": "65f1a2b3c4d5e6f708192a3c",
      "name": "Read member XP",
      "type": "platform-action",
      "order": 1,
      "continueOnFailure": false,
      "retryOnFailure": false,
      "options": {
        "action": "get-user-fields",
        "selectUser": { "type": "email", "value": ["{{65f1a2b3c4d5e6f708192a41.email}}"] },
        "fields": [{ "label": "Total XP", "field": "total_xp" }]
      }
    },
    "revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
    "redactedPaths": []
  }
}
curl --request POST \
  --url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/nodes' \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "type": "platform-action",
    "name": "Read member XP",
    "position": {
      "after": "trigger"
    },
    "options": {
      "action": "get-user-fields",
      "selectUser": {
        "type": "email",
        "value": ["{{65f1a2b3c4d5e6f708192a41.email}}"]
      },
      "fields": [
        {
          "label": "Total XP",
          "field": "total_xp"
        }
      ]
    }
  }'

Response#

HTTP 201 with { node, revision, redactedPaths }, and the new revision in the ETag header. node._id is the id every later step or reference uses.

Read back with GET /{workflowId}: the step must appear at the intended place, with the branch membership you asked for, and the options must match what you sent after normalisation. For an HTTP step with a JSON or raw body, the stored body is a single template element containing the JSON text, which is the shape the canvas shows.

data#object
One step with the workflow revision it left behind.

Eg{ ... }

node#object
A step as returned by the API.

Eg{ ... }

_id#string
Step identifier.

Eg"65f1a2b3c4d5e6f708192a3c"

name#string
Step name.

Eg"Read member XP"

type#string
Step type.

Eg"platform-action"

order#integer
Position on the main line, from 1.

Eg1

continueOnFailure#boolean
Whether a failure stops the run.

Egfalse

retryOnFailure#boolean
Whether the step is retried on failure.

Egfalse

options#object
Stored step options, with every credential replaced by ["__REDACTED__"].

RuleNullable

Eg{ ... }

status#string
Last run state of the step, when one exists.

Eg"success"

nestedNodes#object[]
Present on a router: each branch carries its own workflowNodeIds and nestedNodes.
revision#string
Revision after the write.

Eg"68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"

redactedPaths#string[]
Credential locations replaced in this response.

Eg[]

{
  "data": {
    "node": {
      "_id": "65f1a2b3c4d5e6f708192a3c",
      "name": "Read member XP",
      "type": "platform-action",
      "order": 1,
      "continueOnFailure": false,
      "retryOnFailure": false,
      "options": {
        "action": "get-user-fields",
        "selectUser": {
          "type": "email",
          "value": ["{{65f1a2b3c4d5e6f708192a41.email}}"]
        },
        "fields": [
          {
            "label": "Total XP",
            "field": "total_xp"
          }
        ]
      }
    },
    "revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
    "redactedPaths": []
  }
}

Errors#

Fix the request09

400VALIDATION_FAILEDFix the request
Options do not match the type, a required field is missing, or position.after is not on the main line. Fix the named field.
400NODE_NOT_IN_WORKFLOWFix the request
A router's branch list names a step that is not part of this workflow. Reference only steps this workflow owns.
400LEADERBOARD_NOT_IN_COMMUNITYFix the request
A leaderboard step names another community's leaderboard. Use a leaderboard of the key's community.
400REDACTED_VALUE_WITHOUT_STORED_SECRETFix the request
A marker arrived on a new step, where nothing can be stored yet. Send the real credential.
401AUTHENTICATION_REQUIREDFix the request
No key, or an unknown key. Send a valid key.
401API_KEY_REQUIREDFix the request
A valid admin session was used, because this API accepts community API keys only. Use a community API key.
403API_KEY_PERMISSION_DENIEDFix the request
No workflowBuilder. Grant the permission.
413VALIDATION_FAILEDFix the request
The request body is too large to read. Nothing was written. Send a smaller body; a large import is better split into a create plus step writes.
415UNSUPPORTED_MEDIA_TYPEFix the request
The body was not declared application/json. Send the header.

Fix the data04

404WORKFLOW_NOT_FOUNDFix the data
Unknown workflow id, or another community's workflow. Use an id from this community.
404BRANCH_NOT_FOUNDFix the data
position.branchIndex does not exist on that router. Read the router and use a valid index.
409VERSION_CONFLICTFix the data
You sent an If-Match that is not the current revision. meta.currentRevision carries the live value. Re-read and resend against the new revision.
409WORKFLOW_LOCKEDFix the data
Another write to this workflow held the lock for too long, and this write was not applied. Re-read and retry.

Retry with backoff03

429RATE_LIMITEDRetry with backoff
The community's API rate limit was reached. Wait for the window to reset, then retry.
500INTERNAL_ERRORRetry with backoff
Unexpected failure. Re-read the workflow before retrying.
503LOCK_UNAVAILABLERetry with backoff
The write lock store did not answer, so nothing was written. Retry shortly.

Retries

Adding a step is not idempotent: two accepted calls create two steps. A retry after a timeout can leave a duplicate, so read the workflow and check for the step before resending. A retry with the revision from before the first attempt is refused with 409 VERSION_CONFLICT, which is the safe outcome.

{
  "meta": {
    "status": "error",
    "statusCode": 400,
    "code": "VALIDATION_FAILED",
    "errors": [
      {
        "path": "node.options.url",
        "message": "URL is required"
      }
    ],
    "requestId": "3f4d0e8b-7a6c-4f1e-9b2d-5a7c8e1f0a3b"
  },
  "message": "API request rejected error.",
  "detail": "The request body is not valid.",
  "solution": "Read error detail and try again."
}

Next step#

Get a stepGET/v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId}