Returning.AIDocs
v1

API reference / Workflow Builder

.md

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.

Last updated 28 Oct 2026API v1

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.

FailureMeaningRecovery
409 VERSION_CONFLICTThe source changed since your readRe-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#

workflowId#stringREQUIRED
Workflow identifier: 24 hexadecimal characters, as returned in _id.

Rule^[0-9a-fA-F]{24}$

Eg65f1a2b3c4d5e6f708192a3b

Headers#

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

EgBearer <API_KEY>

If-Match#stringOPTIONAL
Optional. Send the 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 separate PATCH.
  • 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 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.
  • 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
BASH
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:

JSON
{
  "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.

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

Eg{ ... }

_id#string
Workflow identifier.

Eg"65f1a2b3c4d5e6f708192a43"

communityId#string
Community the workflow belongs to.

Eg"65f1a2b3c4d5e6f708192a50"

name#string
Workflow name.

Eg"VIP bonus (Copy)"

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"68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"

redactedPaths#string[]
Every location where a credential was replaced by ["__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

400VALIDATION_FAILEDFix the request
The id is not 24 hexadecimal characters. Use the id exactly as returned.
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.

Fix the data03

404WORKFLOW_NOT_FOUNDFix the data
Unknown id, or another community's workflow. Use an id from this community.
409VERSION_CONFLICTFix the data
You sent an If-Match that is not the current revision. Re-read and duplicate again.
409WORKFLOW_LOCKEDFix the data
Another write to this workflow held the lock for too long, and this write was not applied. Re-read and retry.

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. The copy is written after its steps, so a failure leaves nothing half-created; re-read the list before retrying.
503LOCK_UNAVAILABLERetry with backoff
The write lock store did not answer, so nothing was written. Retry shortly.

Retries

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."
}

Next step#

Export a workflow definitionGET/v1/workflow-builder/data-workflows/{workflowId}/export