POST / needs workflowBuilder.draft: nothing runs until POST /{workflowId}/deploy.revision, and a webhook trigger's generated key is shown once here. A create never changes another workflow and never activates anything.Authorization: Bearer <API_KEY> with 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.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. |
apiKey.enabled: true and no value, the platform mints the key and returns it in this response only.position asks for:{definition}. The document must carry kind: "returningai.workflow", formatVersion: 1 and variant: "data_workflows", or the import is refused:{
"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": []
}
}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.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.| Status and code | When | Recovery |
|---|---|---|
200 with a workflow | Idempotency-Key and the same body were already used. The response is the original workflow, not a second one, and it is a normal read: a webhook key in it is ["__REDACTED__"] | Keep the id from this response; the key is not shown again. |
400 VALIDATION_FAILED | 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. |
400 LEADERBOARD_NOT_IN_COMMUNITY | A leaderboard step in nodes or definition names a leaderboard of another community | Use a leaderboard of the key's community. |
400 REDACTED_VALUE_WITHOUT_STORED_SECRET | 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. |
401 AUTHENTICATION_REQUIRED | No key, or an unknown key | Send a valid key. |
403 API_KEY_PERMISSION_DENIED | No workflowBuilder | Grant the permission. |
409 IDEMPOTENCY_KEY_REUSED | The same Idempotency-Key arrived with a different body | Use a new key for a genuinely new create. |
409 WORKFLOW_LOCKED | An identical request is still being processed | Retry shortly; the first call will produce the workflow. |
415 UNSUPPORTED_MEDIA_TYPE | The body was not declared application/json | Send the header. |
503 LOCK_UNAVAILABLE | The write lock store did not answer, so nothing was written | Retry shortly. |
429 RATE_LIMITED | The community's API rate limit was reached | Wait for the window to reset, then retry. |
500 INTERNAL_ERROR | Unexpected failure | Retry with bounded backoff. |
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__"].409 IDEMPOTENCY_KEY_REUSED; a genuinely new workflow needs a new key.nodes and definition are mutually exclusive. Sending both is refused rather than merged.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.ref in nodes is only a local name for a reference inside the same call. Real step ids come back from the following GET.POST /{workflowId}/validate reports TRIGGER_REQUIRED as a warning. It simply never runs until a trigger is set.["__REDACTED__"], and a PUT /{workflowId}/trigger keeps the stored key rather than accepting a new one.createdAt and updatedAt are equal on a create. They are not the revision, and neither is used for concurrency.POST /{workflowId}/nodes for each further step.POST /{workflowId}/validate to check the whole workflow.POST /{workflowId}/test with mode: "dry-run" to check it without side effects, then POST /{workflowId}/deploy.curl --location 'https://api.returning.ai/v1/workflow-builder/data-workflows' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'Idempotency-Key: create-vip-bonus-001' \
--header 'Content-Type: application/json' \
--data '{
"name": "VIP bonus",
"trigger": {
"type": "webhook",
"name": "Trade bonus trigger",
"options": {
"tokenExchangeEnabled": false,
"apiKey": {
"enabled": true
}
}
}
}'{
"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": []
}
}