# Deploy a workflow

Takes a workflow live.

- Endpoint: `POST https://api.returning.ai/v1/workflow-builder/data-workflows/{workflowId}/deploy`
- Section: Workflow Builder / Tests and deploy
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- 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.
- Guide: generated from the published specification
- Last updated: 28 Oct 2026
- Web page: https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow

## 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

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `workflowId` | `string` | Yes | Workflow identifier: 24 hexadecimal characters, as returned in `_id`. (`^[0-9a-fA-F]{24}$`) |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key: `Bearer <API_KEY>`. |
| `If-Match` | `string` | No | 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. |

### Watch for

- Deploy does not refuse a workflow with no trigger. It deploys, `status` reads `deployed`, and nothing runs until `PUT /{workflowId}/trigger` sets one; the response's `warnings` carry `TRIGGER_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 `OR` or `WHERE` condition groups, a template whose step id is not in the workflow, `retryOnFailure` on a router that pays, an empty imported credential, `POST_LOGIC_REQUIRED`, `NODE_LISTED_TWICE` or `MISSING_SECRET` all arrive as `warnings` and 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 `PUT` that sends a new value. Getting a different key means replacing the trigger's webhook type, which is a trigger change, not a deploy.

### Example request

```bash
curl --request POST \
  --url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/deploy' \
  --header 'Authorization: Bearer <API_KEY>'
```

### More examples

A workflow with a trigger and no warnings:

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/deploy" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
```

```json
{
  "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:

```json
{
  "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." }
    ]
  }
}
```

## 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.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `data` | `object` | - | A workflow after a deploy, with the validator's advisory findings. |
| `data._id` | `string` | - | Workflow identifier. |
| `data.communityId` | `string` | - | Community the workflow belongs to. |
| `data.name` | `string` | - | Workflow name. |
| `data.status` | `string` | - | `draft`, `deployed` or `undeployed`. |
| `data.outgoingWebhookUrl` | `string` | - | Failure webhook URL, when one is set. (Nullable) |
| `data.createdAt` | `string` | - | Creation time. |
| `data.updatedAt` | `string` | - | Last authored change. |
| `data.trigger` | `object` | - | The trigger, or `null` when the workflow has none. (Nullable) |
| `data.trigger._id` | `string` | - | Trigger identifier. Use it as the source of a reference. |
| `data.trigger.name` | `string` | - | Display name. |
| `data.trigger.type` | `string` | - | Trigger type. |
| `data.trigger.status` | `string` | - | Trigger lifecycle state, for example `idle`. |
| `data.trigger.options` | `object` | - | Trigger options. A webhook key is shown only in the response that created it. (Nullable) |
| `data.nodes` | `object[]` | - | Main-line steps in canvas order, routers carrying their branches. |
| `data.nodes._id` | `string` | - | Step identifier. |
| `data.nodes.name` | `string` | - | Step name. |
| `data.nodes.type` | `string` | - | Step type. |
| `data.nodes.order` | `integer` | - | Position on the main line, from 1. |
| `data.nodes.continueOnFailure` | `boolean` | - | Whether a failure stops the run. |
| `data.nodes.retryOnFailure` | `boolean` | - | Whether the step is retried on failure. |
| `data.nodes.options` | `object` | - | Stored step options, with every credential replaced by `["__REDACTED__"]`. (Nullable) |
| `data.nodes.status` | `string` | - | Last run state of the step, when one exists. |
| `data.nodes.nestedNodes` | `object[]` | - | Present on a router: each branch carries its own `workflowNodeIds` and `nestedNodes`. |
| `data.revision` | `string` | - | 64 hexadecimal characters. Send it back as `If-Match`; it also arrives as the `ETag` header. |
| `data.redactedPaths` | `string[]` | - | Every location where a credential was replaced by `["__REDACTED__"]`. |
| `data.warnings` | `object[]` | - | The same advisory findings `POST /{workflowId}/validate` returns, in the same shape. |

### Example response (200)

```json
{
  "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 request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `VALIDATION_FAILED` | The id is malformed. Use the id exactly as returned. |
| 400 | `NODE_NOT_IN_WORKFLOW` | A router branch list names a step that is not in this workflow. Fix the branch lists. |
| 400 | `LEADERBOARD_NOT_IN_COMMUNITY` | A step names a leaderboard this community does not have. Use one of this community's leaderboards. |
| 401 | `AUTHENTICATION_REQUIRED` | No key, or an unknown key. Send a valid key. |
| 401 | `API_KEY_REQUIRED` | A valid admin session was used, because this API accepts community API keys only. Use a community API key. |
| 403 | `API_KEY_PERMISSION_DENIED` | No `workflowBuilder`. Grant the permission. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 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. `meta.currentRevision` carries the live value. Re-read and deploy again. |
| 409 | `WORKFLOW_LOCKED` | Another write to this workflow held the lock for too long, and this write was not applied. Re-read and retry. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 429 | `RATE_LIMITED` | The community's API rate limit was reached. Wait for the window to reset, then retry. |
| 500 | `INTERNAL_ERROR` | Unexpected failure. Re-read the status before retrying. |
| 503 | `LOCK_UNAVAILABLE` | The write lock store did not answer, so nothing was written. Retry shortly. |

**Retries:** 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.

## Next step

- [Undeploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/undeploy-a-workflow.md): `POST /v1/workflow-builder/data-workflows/{workflowId}/undeploy`.
