Duplicate a workflow
Copies a workflow, its trigger and all of its steps into a new workflow named <name> (Copy), stored as a draft with new identifiers.
- Method
- POST
- Path
https://api.returning.ai / v1/ workflow-builder/ data-workflows/ {workflowId}/ duplicate - Permission
- workflowBuilder
- Retries
- Duplicating is not idempotent: two accepted calls create two copies.
Authentication#
- Header
Authorization: Bearer <API_KEY>- Permission
- workflowBuilder
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. |
Behaviour#
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.
The copy is the safe way to change a live workflow: duplicate, edit the copy, test it, deploy the copy, then undeploy the original.
Request#
Path parameters#
_id.Rule^[0-9a-fA-F]{24}$
Eg65f1a2b3c4d5e6f708192a3b
Headers#
Bearer <API_KEY>.EgBearer <API_KEY>
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
Watch for
- The copy always gets the suffix
(Copy); renaming it is a separatePATCH. - A webhook trigger's key is copied as stored, and no new key is revealed in this response, so the copy accepts the same key as the source. A
PUTon the copy keeps that key, so the two workflows share one credential until you replace the copy's webhook trigger (another type and back towebhook) and reconfigure its sender. - The copy keeps the source's failure webhook URL. Two live workflows then report failures to the same receiver; change or clear it on the copy.
- Credentials in step options are copied as stored. They are not re-supplied and not revealed.
- A workflow with no trigger can be duplicated; the copy also has no trigger. It deploys (deploy never refuses on validation) but never runs until a trigger is set.
- Deployment state does not travel: the copy is always a draft.
More examples and details
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/duplicate" \
--header "Authorization: Bearer <API_KEY>" \
--header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
The response is the copy, already read back in full:
{
"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": []
}
}
curl --request POST \
--url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/duplicate' \
--header 'Authorization: Bearer <API_KEY>'
Response#
HTTP 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.
Every identifier is new: the workflow, the trigger and each step. Any reference inside the copy points at the copied step, not at the source step, so the two workflows can be edited independently.
Eg{ ... }
Eg"65f1a2b3c4d5e6f708192a43"
Eg"65f1a2b3c4d5e6f708192a50"
Eg"VIP bonus (Copy)"
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"68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
["__REDACTED__"].Eg[]
{
"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": []
}
}
Errors#
Fix the request04
VALIDATION_FAILEDFix the requestAUTHENTICATION_REQUIREDFix the requestAPI_KEY_REQUIREDFix the requestAPI_KEY_PERMISSION_DENIEDFix the requestworkflowBuilder. Grant the permission.Fix the data03
WORKFLOW_NOT_FOUNDFix the dataVERSION_CONFLICTFix the dataIf-Match that is not the current revision. Re-read and duplicate again.WORKFLOW_LOCKEDFix the dataRetry with backoff03
RATE_LIMITEDRetry with backoffINTERNAL_ERRORRetry with backoffLOCK_UNAVAILABLERetry with backoffRetries
Duplicating is not idempotent: two accepted calls create two copies. A retry after a timeout can therefore leave two workflows named <name> (Copy). Check the list for the extra copy and delete it.
{
"meta": {
"status": "error",
"statusCode": 400,
"code": "VALIDATION_FAILED",
"errors": [
{
"path": "params.workflowId",
"message": "Id must be a 24 character hex string"
}
],
"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."
}