# Workflow Builder API

Build, test, deploy and monitor Data Workflows from your own script or an AI agent, with one community API key. Start with the build journey below, then open the playbook for your job.

- **Base URL:** `https://api.returning.ai/v1/workflow-builder/data-workflows`. Every path on these pages is relative to it, unless it starts with `/v1/`.
- **Credential:** a community API key holding `workflowBuilder`, sent as `Authorization: Bearer <API_KEY>`.
- **AI agents:** the same operations are served as MCP tools. See [Connect an AI agent](https://docs.returning.ai/workflow-builder/connect-an-ai-agent.md).

## What the API is for

The Data Workflows canvas lets an admin build a workflow, test it, take it live and watch what it did. This API does the same from a script or an AI agent, with a community API key: build a workflow, dry-run it without paying anyone, deploy it, read its runs, and retry a failed one.

It is meant for two callers. An integration team automates a repeatable workflow from its own pipeline. An AI agent builds and repairs a workflow conversationally through the MCP endpoint, calling the same operations under the same permission.

## Before the first call

> A test runs for real unless you say otherwise
>
> `mode: "live"` is the default, matching the canvas Test button: it writes member fields, sends mail, calls outside systems and can pay rewards. `mode: "dry-run"` is opt-in and side-effect free, and `sideEffects` in the response tells you which one you got.

- **One permission covers everything.** A community API key with `workflowBuilder` can read, build, test, deploy and retry. There is no separate read, write, test, deploy or live-test permission, and no per-tool split on the MCP endpoint.
- **Edits apply immediately, including to a deployed workflow, and revision checking is optional.** Every workflow and step response carries a `revision`. Send it as `If-Match` (or as `revision` on an MCP tool) and a stale write is refused instead of overwriting a newer save, including a save made in the canvas; omit it and the write wins as the last one. The reverse is never protected: a canvas save does not check an API revision.
- **Credentials are never returned.** A stored credential comes back as `["__REDACTED__"]` with the location listed in `redactedPaths`, whatever the caller's permission.

## The build journey

Follow this order. Each step names the call, and the identifier it leaves behind for the next one.

| Step | Call | What it leaves you |
| --- | --- | --- |
| 1. Discover | [`GET /catalog`](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) | The step types, their option schemas, which have side effects, the limits, and the one permission this API takes |
| 2. Create | [`POST /`](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) | `workflowId`, `revision`, `trigger._id`, and a webhook key if a webhook trigger was requested |
| 3. Add steps | [`POST /{workflowId}/nodes`](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) | A `nodeId` per step and a new `revision` each time |
| 4. Branch | [`POST /{workflowId}/nodes/{nodeId}/branches`](https://docs.returning.ai/api-reference/workflow-builder/add-a-router-branch.md) | A branch index, then steps placed with `position.branchIndex` |
| 5. Check | [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) | `warnings`, advisory: what a run may ignore or misread. No finding here refuses a deploy |
| 6. Test | [`POST /{workflowId}/test`](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) | A per-step trace: executed, simulated, skipped, or failed. Send `mode: "dry-run"` to keep it side-effect free |
| 7. Deploy | [`POST /{workflowId}/deploy`](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) | `status: deployed` and an armed trigger |
| 8. Fire | The trigger itself (a webhook call, an ingest batch, a schedule) | A run |
| 9. Read | [`GET /{workflowId}/runs`](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md), then [`GET /runs/{runId}`](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) | The run outcome and every step result |

Steps 3 and 4 are interchangeable: a step can be created directly inside a branch with `position: {routerNodeId, branchIndex}`.

> Validation is advisory
>
> [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) and the deploy response return their findings as `warnings`, and no finding refuses a deploy. Read the warnings before step 7.

## Choose a playbook

Each playbook turns a common job into the calls that achieve it, including the steps that still need the canvas.

### Prove and change a workflow safely

- [Test, then confirm a live run](https://docs.returning.ai/workflow-builder/test-then-confirm-a-live-run.md): dry-run a workflow that pays members, prove the negative case, then deploy and confirm one triggered run.
- [Bind step inputs](https://docs.returning.ai/workflow-builder/bind-step-inputs.md): reference the trigger and earlier steps, keep defaults harmless, and prove the binding before it pays.
- [Edit safely and copy](https://docs.returning.ai/workflow-builder/edit-safely-and-copy-workflows.md): revision-checked edits, conflicts, export before a risky change, and copying a workflow to another community.
- [Code steps](https://docs.returning.ai/workflow-builder/code-steps-through-the-api.md): what a Code step stores, how to create and change one, and how live and dry-run tests treat it.

### Build a reward or sync workflow

- [Webhook rewards](https://docs.returning.ai/workflow-builder/webhook-workflow-rewards.md): award XP, coins or a numerical field when your own system sends an event.
- [Trading rewards from event ingest](https://docs.returning.ai/workflow-builder/trading-rewards-from-event-ingest.md): receive aggregated trade batches and pay a reward per row.
- [Event ingest and batch status](https://docs.returning.ai/workflow-builder/event-ingest-setup-and-batch-status.md): configure the ingest trigger for your envelope and read a batch through to the member value.
- [Reversible rewards](https://docs.returning.ai/workflow-builder/reversible-rewards.md): a bulk update in reversible mode, so a cancelled trade gets its XP and coins back exactly once.
- [Tier and role sync](https://docs.returning.ai/workflow-builder/keep-member-tier-in-sync.md): converge a member's tier field and role set when the tier changes on your side.

## Identifiers carried between calls

| Identifier | Comes from | Used for |
| --- | --- | --- |
| `<API_KEY>` | A community API key holding `workflowBuilder` | Every call, as `Authorization: Bearer <API_KEY>` |
| `workflowId` | [`POST /`](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) or [`GET /`](https://docs.returning.ai/api-reference/workflow-builder/list-workflows.md) | Every workflow-scoped path |
| `nodeId` | [`POST /{workflowId}/nodes`](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) | Step reads, changes, tests, deletes, moves and branch routes |
| `trigger._id` | [`POST /`](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) and [`PUT /{workflowId}/trigger`](https://docs.returning.ai/api-reference/workflow-builder/create-or-replace-the-trigger.md) | A reference when a step reads the trigger output |
| `revision` | Every response that returns a workflow or a step | `If-Match` on the next write, when you want a concurrent change refused |
| `runId` | [`GET /{workflowId}/runs`](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) | [`GET /runs/{runId}`](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) and [`POST /runs/{runId}/retry`](https://docs.returning.ai/api-reference/workflow-builder/retry-a-failed-run.md) |
| Webhook key | The [`POST /`](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) or [`PUT /{workflowId}/trigger`](https://docs.returning.ai/api-reference/workflow-builder/create-or-replace-the-trigger.md) response that minted it | The sender that calls the webhook trigger |

The trigger id is the exception that surprises authors: a step that reads the trigger's output references `trigger._id`, not the word `trigger`. `position.after` accepts the literal `"trigger"` because it names a place, not a value.

## The revision rule

Every workflow and step response carries `revision`: 64 hexadecimal characters, also sent as the `ETag` header. It changes when authored configuration changes, and it does not change when a run updates a step's status or the trigger's run state.

`If-Match` is optional. Both choices are supported, and they answer different needs:

- **Send it** (the revision you read) and a write whose revision is no longer current is `409 VERSION_CONFLICT`, with `meta.currentRevision` carrying the live value. Re-read, rebuild the change, resend. This is the choice for anything a person also edits, including a workflow open in the canvas.
- **Omit it** and the write applies as the last one. Nothing is checked and nothing is refused, which is the right default for a script that owns its workflow.

A revision covers edits made in the canvas too, because it is computed from the authored fields rather than from a stored counter. An API write based on a revision read before an admin's save is refused rather than overwriting that save.

> Edits to a deployed workflow apply immediately
>
> A deployed workflow accepts every edit, including a delete, and the change applies immediately. There is no draft version to promote, no live-edit header to send, and no undeploy to perform first. That is why the canvas and the API can change the same workflow: treat a live workflow as production configuration.

## Permissions

One permission covers the whole family: `workflowBuilder`. It grants every route and every MCP tool: catalog, reads, export, validate, create, edit, duplicate, import, test in both modes, deploy, undeploy, runs and retry. There is no read/write/test/deploy split to grant, and no per-tool split inside the MCP endpoint: a key with `workflowBuilder` sees all 27 tools.

That means a key is all-or-nothing. Grant it to an integration or an agent you would let change a live workflow, and put the safety where the API no longer does:

- **Tell the agent to pass `mode: "dry-run"`.** Live is the default for both test tools, and live runs really write, send and pay. The same instruction covers a retry: there is no dry retry, so dry-run the workflow before queueing one.
- **Tell the agent to send `If-Match`** (or `revision` on a tool call). Without it a write silently wins, including over a change a person made in the canvas a moment earlier.
- **Tell the agent to read `warnings`.** Validation is advisory: `validate` and `deploy` both return the findings, and neither refuses on a finding, so the warnings are the only thing between a misconfigured workflow and a live run.

The existing `dataWorkflows` permission keeps its old meaning (event ingest status read) and grants nothing here. An admin session token is refused: this API accepts community API keys only. The reverse also holds: the canvas's own routes refuse a community API key with `403 API_KEY_NOT_ALLOWED`, so this API is the way to build workflows with a key.

## Field keys come from the catalog

A platform-action step names the member field it reads or writes. Those keys differ by action and by community: a read uses `total_xp` and `total_coins`, a write uses `xp` and `coin`, and a community's own custom fields use their own keys.

[`GET /catalog`](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) returns the allowed keys as `platformActionFields.get` and `platformActionFields.post`. Each entry carries the key to store, the label the platform fills in when you omit one, and whether the field is builtin or custom. A read action accepts `total_xp`, `total_coins`, `xp` and `coin`; a write action accepts `xp` and `coin`; both accept the community's own custom fields. Read the list rather than guessing:

- A key outside the list is stored as sent, and [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) reports `UNKNOWN_USER_FIELD` as a warning. Nothing refuses the write, so take the key from this list: a step with a key that is not a real field reads or writes nothing.
- A workflow that already stores a field the backend does not compute deploys too. `UNKNOWN_USER_FIELD` is a warning in both cases.
- A read step's result is keyed by the field's label, so a later step references that label. A label you send is kept exactly as you sent it; only a missing label is filled from the catalog. A reference that names a label the step does not produce is reported as `REFERENCE_UNKNOWN_FIELD` by [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md), and resolves to nothing at run time.

## Secrets

- A stored credential is returned as the whole array `["__REDACTED__"]`, even where the stored value is a plain string, and every replaced location is listed in `redactedPaths`. Redaction applies to the workflow read, the step read, the export, run results and dry-run output.
- Redaction covers HTTP authentication values, headers, parameters and body keys whose names look like credentials, credential-named URL query parameters, JSON body keys inside a resolved payload, and the trigger's key fields.
- Booleans and numbers are never redacted, so a webhook's `tokenExchangeEnabled` and `apiKey.enabled` stay readable. That is how a caller learns the authentication mode without seeing the key.
- To change something else and keep a credential, send the marker back at that exact path. A marker where nothing is stored is `400 REDACTED_VALUE_WITHOUT_STORED_SECRET`.
- A stored credential is restored by path, so it does follow a URL to another host: repoint an HTTP step and keep the marker, and the stored credential is sent to the new host. Read the `url` you stored before the first run there, and replace the credential rather than echoing the marker when the host is not one you control.
- An import blanks the credentials it could not read (a node's, not a trigger's) and lists them in `secretsToFill`. Validation reports each one as `MISSING_SECRET` until it is supplied, as a warning: an imported workflow can be deployed with a credential still empty, and the step will run without it. Fill every path first.

> A webhook key is shown once
>
> A webhook trigger key is minted once, when the trigger is created, and shown once in that response. Every later read redacts it, and a `PUT` keeps the stored key: a value you send is ignored, so a lost key cannot be rewritten. The only way to a different key is to stop having a webhook trigger, by setting another type and then `webhook` again, which stores no key in between and therefore mints a new one. Treat that as replacing the trigger's authentication rather than rotating a key, and reconfigure the sender.

## What this API protects, and what it does not

Two protections are enforced here, and they are not negotiable:

- **Tenant scoping.** The community comes from the API key. No route and no MCP tool takes a community identifier, so there is nothing to spoof, and every read and write is filtered by the key's community. Another community's workflow, step, branch or run id answers `404` exactly as an id that does not exist does, so a key cannot be used to discover one. A leaderboard or a branch member outside the community is refused with `400 LEADERBOARD_NOT_IN_COMMUNITY` or `400 NODE_NOT_IN_WORKFLOW`.
- **Secret redaction.** A stored credential never leaves in a response, whoever is asking and whatever the mode. That includes the workflow read, the step read, the export, run results and dry-run output.

Nothing else is gated. One permission, no confirmation header, no live-edit gate, no deploy refusal on validation findings, and an optional revision check: the API does what the canvas does and leaves the judgment to the caller. Two consequences worth stating plainly:

- **A key with `workflowBuilder` can spend money and change a live workflow.** There is no narrower key to hand an agent you do not trust with that. The `mode` it passes and the `If-Match` it sends are the controls, and they live in the caller's own instructions.
- **Security controls that belong to the platform are not enforced here.** Admin access, the sandbox a Code step executes in, and how API-key permissions are granted are the platform's own controls, owned outside this API; this API neither changes them nor depends on them. What it guarantees is the two protections above.

## What the API cannot do today

- **Custom Workflows are not covered.** This base path is the Data Workflows canvas. The older Custom Workflows canvas keeps its own admin-only routes.
- **A step cannot move between branches in one call.** Moving within the main line has a route; moving into or out of a branch is a delete plus a create.
- **Nothing but the two tenant checks refuses a deploy.** A router branch that lists a step from another workflow (`400 NODE_NOT_IN_WORKFLOW`) and a leaderboard from another community (`400 LEADERBOARD_NOT_IN_COMMUNITY`) are refused. A router that uses `OR` or `WHERE` condition groups, a template whose step id is not in the workflow, one step listed in two branch lists, a write step without `logic`, `retryOnFailure` on a router whose branches pay, an empty imported credential and a workflow with no trigger all deploy. [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) and the deploy response both return them as `warnings`; the warnings are the whole safety net, so read them.
- **A whole workflow cannot be replaced in place.** Import creates a new workflow; changing one means changing its steps.
- **There is no draft version of a deployed workflow.** Edits apply to it immediately, including a delete. Duplicate it, edit the copy, dry-run it and deploy the copy if you want a reviewable change.
- **A secret an author typed inside a raw HTTP body template is not redacted**, exactly as the canvas shows it today. Keep credentials in the credential fields.
- **A field key or a label is checked, not refused.** A field key is matched against the community's real fields, and a reference against the labels its step produces. A typo in either is stored, and [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) reports `UNKNOWN_USER_FIELD` or `REFERENCE_UNKNOWN_FIELD` as a warning. At run time a bad key reads or writes nothing, and a bad label reference resolves to nothing. A read step's result is keyed by the field's label, for example `Total XP`. See [Field keys come from the catalog](#field-keys-come-from-the-catalog).

## Shared error envelope

Every error is ordinary JSON with the same shape, whatever the status. Every error except the few on `/mcp` below looks like this:

`Error response`

```json
{
  "meta": {
    "status": "error",
    "statusCode": 409,
    "code": "VERSION_CONFLICT",
    "currentRevision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
    "requestId": "3f4d0e8b-7a6c-4f1e-9b2d-5a7c8e1f0a3b"
  },
  "message": "API request rejected error.",
  "detail": "The workflow changed since this revision was read. Re-read it and retry.",
  "solution": "Read error detail and try again."
}
```

`meta.code` is the machine-readable part; `detail` is the human explanation; `meta.requestId` correlates the call with a support request. Extra fields sit in `meta` (`errors` on a failed validation, `currentRevision` on a conflict, `paths` on a redaction refusal, `nodeIds` on a cross-workflow reference).

Statuses used across the family:

| Status | Means |
| --- | --- |
| `400` | A request the API cannot interpret |
| `401` | A credential problem |
| `403` | The permission the key does not hold |
| `404` | An object that does not exist in this community |
| `409` | A conflict |
| `413` | A body too large to read (`meta.code` is `VALIDATION_FAILED`) |
| `415` | A body that is not declared JSON |
| `429` | The community's API rate limit is reached (`meta.code` is `RATE_LIMITED`) |
| `500` | An unexpected failure |
| `503` | A write that was not attempted |

There is no `422` and no `428`: nothing refuses a deploy on validation, and `If-Match` is optional.

The exceptions are all on `/mcp`: a `405` carries `code`, `detail` and `requestId` at the top level, and the MCP transport's own refusals (`406` when `Accept` lacks `application/json` or `text/event-stream`, `400` for a body that is not a JSON-RPC message) carry a JSON-RPC `error` object. The [MCP endpoint page](https://docs.returning.ai/api-reference/workflow-builder/call-the-workflow-tools-over-mcp.md) shows both.

## Next steps

- The endpoint pages in the API reference carry the full contract for each call, starting with [List step types and trigger types](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md).
- [Connect an AI agent](https://docs.returning.ai/workflow-builder/connect-an-ai-agent.md) covers the MCP endpoint, the client configuration and the tool list.

## Endpoints on this page

- GET [List step types and trigger types](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) `/catalog`
- POST [Create a workflow](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) `/`
- POST [Add a step](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) `/{workflowId}/nodes`
- POST [Add a router branch](https://docs.returning.ai/api-reference/workflow-builder/add-a-router-branch.md) `/{workflowId}/nodes/{nodeId}/branches`
- POST [Validate a stored workflow](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) `/{workflowId}/validate`
- POST [Test a whole workflow](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) `/{workflowId}/test`
- POST [Deploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) `/{workflowId}/deploy`
- GET [List the runs of a workflow](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) `/{workflowId}/runs`
- GET [Get one run](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) `/runs/{runId}`
- GET [List workflows](https://docs.returning.ai/api-reference/workflow-builder/list-workflows.md) `/`
- PUT [Create or replace the trigger](https://docs.returning.ai/api-reference/workflow-builder/create-or-replace-the-trigger.md) `/{workflowId}/trigger`
- POST [Retry a failed run](https://docs.returning.ai/api-reference/workflow-builder/retry-a-failed-run.md) `/runs/{runId}/retry`
- POST [Call the workflow tools over MCP](https://docs.returning.ai/api-reference/workflow-builder/call-the-workflow-tools-over-mcp.md) `/mcp`

Paths that start with `/v1/` are full paths; the rest are relative to `https://api.returning.ai/v1/workflow-builder/data-workflows`.
