Deploy a workflow
Takes a workflow live.
- Method
- POST
- Path
https://api.returning.ai / v1/ workflow-builder/ data-workflows/ {workflowId}/ deploy - Permission
- workflowBuilder
- Retries
- Deploying an already deployed workflow is idempotent in effect: the second call stores the same status and rebuilds the same step order.
Authentication#
- Header
Authorization: Bearer <API_KEY>- Permission
- workflowBuilder
Authorization: Bearer <API_KEY> with workflowBuilder, on a workflow of the key's community. If-Match is optional: send the revision you read and a stale value is refused with 409 VERSION_CONFLICT, omit it and the deploy applies as the last write. Deploying a workflow that is already deployed is allowed, and needs no extra header.
There is no separate deploy permission: the one workflowBuilder permission covers building, testing, deploying and reading. If you want a workflow to be reviewable before it runs, that has to be a policy on your side, or a copy of the workflow deployed while the original stays undeployed.
| Failure | Meaning | Recovery |
|---|---|---|
404 WORKFLOW_NOT_FOUND | Unknown id, or another community's workflow | Use an id from this community. |
400 NODE_NOT_IN_WORKFLOW | A router branch lists a step that is not part of this workflow | Fix the branch lists; this is a tenant check and one of the two things that can refuse a deploy. |
400 LEADERBOARD_NOT_IN_COMMUNITY | A step names a leaderboard this community does not have | Use a leaderboard of the key's community; the other refusal. |
Behaviour#
Deploy stores the deployed state, rebuilds the step order from the stored steps, and arms the trigger.
Validation is advisory: deploy runs the same static check as POST /{workflowId}/validate, returns its findings as warnings in the response, and never refuses because of them. A workflow with a router it cannot evaluate, a dangling reference, an empty imported credential or no trigger at all still deploys; it may then fail or do nothing at run time, which is exactly what the warnings describe.
After a deploy, the trigger starts runs: a webhook accepts its calls, an ingest accepts batches, a schedule runs on its cadence.
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
- Deploy does not refuse a workflow with no trigger. It deploys,
statusreadsdeployed, and nothing runs untilPUT /{workflowId}/triggersets one; the response'swarningscarryTRIGGER_REQUIRED. - Deploy rebuilds the workflow's step order from the stored steps: main line by order, then each router's branch members. The order a client last sent is not trusted.
- A deployed workflow accepts every edit, including a delete, and the change applies to the next run immediately. Copy, edit the copy, dry-run it, then deploy the copy if you want a reviewable change.
- No validation finding refuses a deploy: a router that uses
ORorWHEREcondition groups, a template whose step id is not in the workflow,retryOnFailureon a router that pays, an empty imported credential,POST_LOGIC_REQUIRED,NODE_LISTED_TWICEorMISSING_SECRETall arrive aswarningsand the workflow goes live. The warnings are the whole safety net, so read them. - Because the validator reuses the run path's own reference and operator rules, a warning names exactly what a run will mishandle: a condition the platform evaluates as always true, a reference that resolves to nothing, a retry that repeats a payout.
- A Code step deploys like any other step. Its source runs in the platform sandbox on every run, so dry-run it first if you have not run that source here before.
- Deploying does not send a test event and does not prove the trigger is reachable from outside. Fire it for real.
- A webhook key cannot be changed by a deploy or by a
PUTthat sends a new value. Getting a different key means replacing the trigger's webhook type, which is a trigger change, not a deploy.
More examples and details
A workflow with a trigger and no warnings:
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/deploy" \
--header "Authorization: Bearer <API_KEY>" \
--header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
{
"data": {
"_id": "65f1a2b3c4d5e6f708192a3b",
"communityId": "65f1a2b3c4d5e6f708192a50",
"name": "VIP bonus",
"status": "deployed",
"outgoingWebhookUrl": null,
"createdAt": "2026-10-24T09:15:00.000Z",
"updatedAt": "2026-10-24T13:02:41.000Z",
"trigger": { "_id": "65f1a2b3c4d5e6f708192a41", "name": "Trade bonus trigger", "type": "event-ingest", "status": "idle", "options": { "batchIdPath": "metadata.batch_id", "recordIdPaths": ["trade_id"] } },
"nodes": [],
"revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
"redactedPaths": [],
"warnings": []
}
}
A deploy that carried warnings. The workflow is live; the warnings name what a run may mishandle:
{
"data": {
"_id": "65f1a2b3c4d5e6f708192a3b",
"communityId": "65f1a2b3c4d5e6f708192a50",
"name": "VIP bonus",
"status": "deployed",
"outgoingWebhookUrl": null,
"createdAt": "2026-10-24T09:15:00.000Z",
"updatedAt": "2026-10-24T13:02:41.000Z",
"trigger": { "_id": "65f1a2b3c4d5e6f708192a41", "name": "Trades", "type": "event-ingest", "status": "idle", "options": { "batchIdPath": "metadata.batch_id", "recordIdPaths": ["trade_id"] } },
"nodes": [],
"revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
"redactedPaths": [],
"warnings": [
{ "nodeId": "65f1a2b3c4d5e6f708192a3e", "path": "options.branches[0].conditions[0].operator", "code": "ROUTER_OPERATOR_UNSAFE", "message": "The runtime treats OR and WHERE condition groups as always true." },
{ "nodeId": "65f1a2b3c4d5e6f708192a3d", "path": "options.fields[0].value", "code": "REFERENCE_MAY_BE_UNSET", "message": "This reads a branch step that may not run; the value can be unset." }
]
}
}
curl --request POST \
--url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/deploy' \
--header 'Authorization: Bearer <API_KEY>'
Response#
HTTP 200 with the workflow, status: "deployed" and the new revision in the body and the ETag header. The trigger reads idle, which is the armed state. The body also carries warnings, the same advisory findings POST /{workflowId}/validate returns, so an agent that skipped validate still sees them. Read them, because nothing else will stop the workflow from running in a shape the platform warns about.
Read back with GET /{workflowId}: status must be deployed, and the trigger must be the type you expect. Then prove the workflow runs: fire the trigger with a synthetic event and read the run with GET /{workflowId}/runs. A deploy alone proves configuration, not execution.
Eg{ ... }
Eg"65f1a2b3c4d5e6f708192a3b"
Eg"65f1a2b3c4d5e6f708192a50"
Eg"VIP bonus"
draft, deployed or undeployed.Eg"deployed"
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[]
POST /{workflowId}/validate returns, in the same shape.Eg[]
{
"data": {
"_id": "65f1a2b3c4d5e6f708192a3b",
"communityId": "65f1a2b3c4d5e6f708192a50",
"name": "VIP bonus",
"status": "deployed",
"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": [],
"warnings": []
}
}
Errors#
Fix the request06
VALIDATION_FAILEDFix the requestNODE_NOT_IN_WORKFLOWFix the requestLEADERBOARD_NOT_IN_COMMUNITYFix 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. meta.currentRevision carries the live value. Re-read and deploy again.WORKFLOW_LOCKEDFix the dataRetry with backoff03
RATE_LIMITEDRetry with backoffINTERNAL_ERRORRetry with backoffLOCK_UNAVAILABLERetry with backoffRetries
Deploying an already deployed workflow is idempotent in effect: the second call stores the same status and rebuilds the same step order. It returns a new revision only if something changed. A retry after a timeout is safe in the sense that it cannot create a second workflow. Check the status first: if the workflow reads deployed, the first call landed.
{
"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."
}