Returning.AIDocs
v1

API reference / Workflow Builder

.md

Create a workflow

Creates a draft workflow, its single trigger, and optionally all of its steps, in one call.

Last updated 28 Oct 2026API v1

Method
POST
Path
https://api.returning.ai/v1/workflow-builder/data-workflows
Permission
workflowBuilder
Retries
Without Idempotency-Key, a retry creates a second workflow.

Authentication#

Header
Authorization: Bearer <API_KEY>
Permission
workflowBuilder

The new workflow belongs to the key's community; there is no community field in the body and no way to create into another community.

The body must be declared application/json. Any other content type is refused with 415 before validation.

FailureMeaningRecovery
401 AUTHENTICATION_REQUIREDNo key, or an unknown keySend a valid key.
403 API_KEY_PERMISSION_DENIEDThe key lacks workflowBuilderGrant the permission.

Behaviour#

The workflow is always stored as draft: nothing runs until POST /{workflowId}/deploy.

The response carries the new workflow with its first revision, and a webhook trigger's generated key is shown once here. A create never changes another workflow and never activates anything.

Request#

Headers#

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

EgBearer <API_KEY>

Idempotency-Key#stringOPTIONAL
Optional client-chosen key. The same key with the same body returns the original result instead of creating a second object.

Egcreate-vip-bonus-001

Content-Type#stringREQUIRED
Request body format.

Ruleapplication/json

Body#

name#stringREQUIRED
Workflow name.

Rule1-200 chars

Eg"VIP bonus"

trigger#objectOPTIONAL
Optional trigger, created with the workflow in the same call.

RuleNullable

Eg{ ... }

type#stringREQUIRED
Trigger type.

Ruletime, webhook, visit-community, join-community, custom-field-update, conditions-trigger, event-ingest

Eg"webhook"

name#stringOPTIONAL
Display name. Defaults to Trigger.

RuleMax 200 chars

Eg"Trade bonus trigger"

options#objectOPTIONAL
Trigger options, validated against the trigger type's schema from GET /catalog.

RuleNullable

Eg{"tokenExchangeEnabled": false, "apiKey": {"enabled": true}}

nodes#object[]OPTIONAL
Optional steps, created in order. At most 200.

RuleMax 200 items

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"

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"

position#objectOPTIONAL
Where the step goes. Omit to append to the main line.
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

definition#objectOPTIONAL
Optional export document to import instead of nodes.

Eg{ ... }

Watch for

  • nodes and definition are mutually exclusive. Sending both is refused rather than merged.
  • A router branch member that is not one of this workflow's steps is not refused when the workflow is created. POST /{workflowId}/validate reports it as NODE_NOT_IN_WORKFLOW, and deploy refuses it with 400 NODE_NOT_IN_WORKFLOW, so reference only steps this document owns.
  • nodes are created in array order. position is accepted there for shape compatibility but the array order is what a create stores.
  • A ref in nodes is only a local name for a reference inside the same call. Real step ids come back from the following GET.
  • The trigger is optional. A workflow with no trigger can be created and edited, and it can be deployed: deploy never refuses on validation, and POST /{workflowId}/validate reports TRIGGER_REQUIRED as a warning. It simply never runs until a trigger is set.
  • A webhook key is returned in clear exactly once, in this response. Store it when you see it: every later read returns ["__REDACTED__"], and a PUT /{workflowId}/trigger keeps the stored key rather than accepting a new one.
  • The response body is never logged by the platform, but it can hold a key. Keep the raw response out of your own logs.
  • createdAt and updatedAt are equal on a create. They are not the revision, and neither is used for concurrency.
More examples and details

A name and a webhook trigger. With apiKey.enabled: true and no value, the platform mints the key and returns it in this response only.

BASH
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"name":"VIP bonus","trigger":{"type":"webhook","name":"Trade bonus trigger","options":{"tokenExchangeEnabled":false,"apiKey":{"enabled":true}}}}'

A trigger and one step in a single call. Steps are created in the order given, not in the order a position asks for:

BASH
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"name":"VIP bonus","trigger":{"type":"event-ingest","name":"Trades","options":{"batchIdPath":"metadata.batch_id","recordIdPaths":["trade_id"]}},"nodes":[{"type":"platform-action","name":"Read member XP","ref":"read-member","options":{"action":"get-user-fields","selectUser":{"type":"email","value":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"fields":[{"label":"Total XP","field":"total_xp"}]}}]}'

A definition exported from another workflow, posted as {definition}. The document must carry kind: "returningai.workflow", formatVersion: 1 and variant: "data_workflows", or the import is refused:

BASH
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"name":"VIP bonus (imported)","definition":{"kind":"returningai.workflow","formatVersion":1,"variant":"data_workflows","workflow":{"name":"VIP bonus"},"trigger":null,"nodes":[]}}'

The response of the first example:

JSON
{
  "data": {
    "_id": "65f1a2b3c4d5e6f708192a3b",
    "communityId": "65f1a2b3c4d5e6f708192a50",
    "name": "VIP bonus",
    "status": "draft",
    "outgoingWebhookUrl": null,
    "createdAt": "2026-10-24T09:15:00.000Z",
    "updatedAt": "2026-10-24T09:15:00.000Z",
    "trigger": {
      "_id": "65f1a2b3c4d5e6f708192a41",
      "name": "Trade bonus trigger",
      "type": "webhook",
      "status": "idle",
      "options": { "tokenExchangeEnabled": false, "apiKey": { "enabled": true, "value": "<WEBHOOK_KEY>" } }
    },
    "nodes": [],
    "revision": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c",
    "redactedPaths": [],
    "secretsToFill": []
  }
}
curl --request POST \
  --url https://api.returning.ai/v1/workflow-builder/data-workflows \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "VIP bonus",
    "trigger": {
      "type": "webhook",
      "name": "Trade bonus trigger",
      "options": {
        "tokenExchangeEnabled": false,
        "apiKey": {
          "enabled": true
        }
      }
    }
  }'

Response#

HTTP 201 with the workflow in data, its revision both in the body and in the ETag header. Keep _id, revision, trigger._id and, for a webhook trigger, the generated key.

Read back with GET /{workflowId}: the name, status, trigger type and step count must match what you sent. That read also proves the webhook key is now redacted, which is the expected state after the one reveal.

secretsToFill lists credential paths that an imported definition left empty. It is empty for a plain create.

data#object
A workflow with its trigger, its steps in canvas order and its revision.

Eg{ ... }

_id#string
Workflow identifier.

Eg"65f1a2b3c4d5e6f708192a3b"

communityId#string
Community the workflow belongs to.

Eg"65f1a2b3c4d5e6f708192a50"

name#string
Workflow name.

Eg"VIP bonus"

status#string
draft, deployed or undeployed.

Eg"draft"

outgoingWebhookUrl#string
Failure webhook URL, when one is set.

RuleNullable

Egnull

createdAt#string
Creation time.

Eg"2026-10-24T09:15:00.000Z"

updatedAt#string
Last authored change.

Eg"2026-10-24T09:20:11.000Z"

trigger#object
The trigger, or null when the workflow has none.

RuleNullable

Eg{ ... }

_id#string
Trigger identifier. Use it as the source of a reference.

Eg"65f1a2b3c4d5e6f708192a41"

name#string
Display name.

Eg"Trade bonus trigger"

type#string
Trigger type.

Eg"webhook"

status#string
Trigger lifecycle state, for example idle.

Eg"idle"

options#object
Trigger options. A webhook key is shown only in the response that created it.

RuleNullable

Eg{ ... }

nodes#object[]
Main-line steps in canvas order, routers carrying their branches.

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
64 hexadecimal characters. Send it back as If-Match; it also arrives as the ETag header.

Eg"162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"

redactedPaths#string[]
Every location where a credential was replaced by ["__REDACTED__"].

Eg[]

{
  "data": {
    "_id": "65f1a2b3c4d5e6f708192a3b",
    "communityId": "65f1a2b3c4d5e6f708192a50",
    "name": "VIP bonus",
    "status": "draft",
    "outgoingWebhookUrl": null,
    "createdAt": "2026-10-24T09:15:00.000Z",
    "updatedAt": "2026-10-24T09:20:11.000Z",
    "trigger": {
      "_id": "65f1a2b3c4d5e6f708192a41",
      "name": "Trade bonus trigger",
      "type": "webhook",
      "status": "idle",
      "options": {
        "tokenExchangeEnabled": false,
        "apiKey": {
          "enabled": false
        },
        "aesKey": {
          "enabled": false
        }
      }
    },
    "nodes": [
      {
        "_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": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c",
    "redactedPaths": []
  }
}

Errors#

Fix the request08

400VALIDATION_FAILEDFix the request
Missing or empty name, both nodes and definition, more than 200 nodes, an unknown trigger type, or invalid step options. errors[].path names the field. Fix the named field.
400LEADERBOARD_NOT_IN_COMMUNITYFix the request
A leaderboard step in nodes or definition names a leaderboard of another community. Use a leaderboard of the key's community.
400REDACTED_VALUE_WITHOUT_STORED_SECRETFix the request
The body contains the redaction marker where nothing is stored, for example a copied export posted as a create. Send the real credential, or import the definition so the marker is blanked and listed.
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 data02

409IDEMPOTENCY_KEY_REUSEDFix the data
The same Idempotency-Key arrived with a different body. Use a new key for a genuinely new create.
409WORKFLOW_LOCKEDFix the data
An identical request is still being processed. Retry shortly; the first call will produce the workflow.

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. Retry with bounded backoff.
503LOCK_UNAVAILABLERetry with backoff
The write lock store did not answer, so nothing was written. Retry shortly.

Retries

Without Idempotency-Key, a retry creates a second workflow. With it, the same key and the same body return the first workflow as 200 instead of creating another, for 24 hours. That replay is the read of the existing workflow, so a webhook key that the first 201 returned in clear comes back as ["__REDACTED__"]. The stored idempotency record holds a digest of the body, never the body itself, so a credential in a create body is not readable from it. A different body under the same key is refused with 409 IDEMPOTENCY_KEY_REUSED; a genuinely new workflow needs a new key.

{
  "meta": {
    "status": "error",
    "statusCode": 400,
    "code": "VALIDATION_FAILED",
    "errors": [
      {
        "path": "nodes[0].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 workflowGET/v1/workflow-builder/data-workflows/{workflowId}