POST /{workflowId}/duplicate needs workflowBuilder.<name> (Copy), stored as a draft with new identifiers. Every reference is rewired to the copies: a {nodeId, field} reference object, a field value that names a step, a router branch's step list, and a {{<stepId>.path}} template, including one inside a JSON body template or a URL. A template is paired with the step it named, not with a position, so a step inserted after the trigger or a step inside a router branch still lands on the right copy. The copy references only its own trigger and steps, so it is deployable without editing a reference.Authorization: Bearer <API_KEY> with workflowBuilder, on a workflow of the key's community. The call reads the source and writes a new draft, and the source is not modified.If-Match is optional. Send the revision you read and the copy is made from that state, with 409 VERSION_CONFLICT instead of a copy of a workflow that changed under you. Without it, the copy is taken from whatever is current.| Failure | Meaning | Recovery |
|---|---|---|
409 VERSION_CONFLICT | The source changed since your read | Re-read and duplicate again. |
{
"data": {
"_id": "65f1a2b3c4d5e6f708192a43",
"communityId": "65f1a2b3c4d5e6f708192a50",
"name": "VIP bonus (Copy)",
"status": "draft",
"outgoingWebhookUrl": null,
"createdAt": "2026-10-24T12:01:03.000Z",
"updatedAt": "2026-10-24T12:01:03.000Z",
"trigger": { "_id": "65f1a2b3c4d5e6f708192a47", "name": "Trade bonus trigger", "type": "event-ingest", "status": "idle", "options": { "batchIdPath": "metadata.batch_id", "recordIdPaths": ["trade_id"] } },
"nodes": [],
"revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
"redactedPaths": []
}
}201 with the new workflow in data, its new revision in the body and the ETag header. The copy is the readback: it shows the new ids, the new name and status: draft.| Status and code | When | Recovery |
|---|---|---|
400 VALIDATION_FAILED | The id is not 24 hexadecimal characters | Use the id exactly as returned. |
401 AUTHENTICATION_REQUIRED | No key, or an unknown key | Send a valid key. |
403 API_KEY_PERMISSION_DENIED | No workflowBuilder | Grant the permission. |
404 WORKFLOW_NOT_FOUND | Unknown id, or another community's workflow | Use an id from this community. |
409 VERSION_CONFLICT | You sent an If-Match that is not the current revision | Re-read and duplicate again. |
429 RATE_LIMITED | The community's API rate limit was reached | Wait for the window to reset, then retry. |500 INTERNAL_ERROR | Unexpected failure | The copy is written after its steps, so a failure leaves nothing half-created; re-read the list before retrying. |503 LOCK_UNAVAILABLE | The write lock store did not answer, so nothing was written | Retry shortly. |<name> (Copy). Check the list for the extra copy and delete it.(Copy); renaming it is a separate PATCH.PUT on the copy keeps that key, so the two workflows share one credential until you replace the copy's webhook trigger (another type and back to webhook) and reconfigure its sender.PATCH /{workflowId} to rename the copy.PUT /{workflowId}/trigger twice, another type and then webhook, when the copy must not share the source's key. The second call mints a new key; reconfigure the copy's sender.POST /{workflowId}/test with mode: "dry-run" then POST /{workflowId}/deploy.curl --location --request POST 'https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/duplicate' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c'{
"data": {
"_id": "65f1a2b3c4d5e6f708192a43",
"communityId": "65f1a2b3c4d5e6f708192a50",
"name": "VIP bonus (Copy)",
"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": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
"redactedPaths": []
}
}