Create a workflow
Creates a draft workflow, its single trigger, and optionally all of its steps, in one call.
- 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.
| Failure | Meaning | Recovery |
|---|---|---|
401 AUTHENTICATION_REQUIRED | No key, or an unknown key | Send a valid key. |
403 API_KEY_PERMISSION_DENIED | The key lacks workflowBuilder | Grant 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#
Body#
Rule1-200 chars
Eg"VIP bonus"
RuleNullable
Eg{ ... }
Ruletime, webhook, visit-community, join-community, custom-field-update, conditions-trigger, event-ingest
Eg"webhook"
Trigger.RuleMax 200 chars
Eg"Trade bonus trigger"
GET /catalog.RuleNullable
Eg{"tokenExchangeEnabled": false, "apiKey": {"enabled": true}}
RuleMax 200 items
Rulehttp, platform-action, math, send-email, send-emails, code, router, users-bulk-update
Eg"platform-action"
RuleMax 200 chars
Eg"Read member XP"
GET /catalog.Eg{ ... }
false.Egfalse
false.Egfalse
node-<index>.RuleMax 120 chars
Eg"read-member"
trigger for the first position.Eg"trigger"
Eg"65f1a2b3c4d5e6f708192a3e"
RuleMin 0
Eg0
nodes.Eg{ ... }
Watch for
nodesanddefinitionare 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}/validatereports it asNODE_NOT_IN_WORKFLOW, and deploy refuses it with400 NODE_NOT_IN_WORKFLOW, so reference only steps this document owns. nodesare created in array order.positionis accepted there for shape compatibility but the array order is what a create stores.- A
refinnodesis only a local name for a reference inside the same call. Real step ids come back from the followingGET. - 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}/validatereportsTRIGGER_REQUIREDas 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 aPUT /{workflowId}/triggerkeeps 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.
createdAtandupdatedAtare 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.
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:
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:
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:
{
"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.
Eg{ ... }
Eg"65f1a2b3c4d5e6f708192a3b"
Eg"65f1a2b3c4d5e6f708192a50"
Eg"VIP bonus"
draft, deployed or undeployed.Eg"draft"
RuleNullable
Egnull
Eg"2026-10-24T09:15:00.000Z"
Eg"2026-10-24T09:20:11.000Z"
null when the workflow has none.RuleNullable
Eg{ ... }
Eg"65f1a2b3c4d5e6f708192a41"
Eg"Trade bonus trigger"
Eg"webhook"
idle.Eg"idle"
RuleNullable
Eg{ ... }
Eg[ ... ]
Eg"65f1a2b3c4d5e6f708192a3c"
Eg"Read member XP"
Eg"platform-action"
Eg1
Egfalse
Egfalse
["__REDACTED__"].RuleNullable
Eg{ ... }
Eg"success"
workflowNodeIds and nestedNodes.If-Match; it also arrives as the ETag header.Eg"162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
["__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
VALIDATION_FAILEDFix the requestname, 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.LEADERBOARD_NOT_IN_COMMUNITYFix the requestnodes or definition names a leaderboard of another community. Use a leaderboard of the key's community.REDACTED_VALUE_WITHOUT_STORED_SECRETFix the requestAUTHENTICATION_REQUIREDFix the requestAPI_KEY_REQUIREDFix the requestAPI_KEY_PERMISSION_DENIEDFix the requestworkflowBuilder. Grant the permission.VALIDATION_FAILEDFix the requestUNSUPPORTED_MEDIA_TYPEFix the requestapplication/json. Send the header.Fix the data02
IDEMPOTENCY_KEY_REUSEDFix the dataIdempotency-Key arrived with a different body. Use a new key for a genuinely new create.WORKFLOW_LOCKEDFix the dataRetry with backoff03
RATE_LIMITEDRetry with backoffINTERNAL_ERRORRetry with backoffLOCK_UNAVAILABLERetry with backoffRetries
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."
}