Returning.AIDocs
v1

Guides / Workflow Builder

.md

Workflow Builder API

The base URL is https://api.returning.ai/v1/workflow-builder/data-workflows. Every path below is relative to it.

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.

Four properties are worth knowing before the first call:

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

StepCallWhat it leaves you
1. DiscoverGET /catalogThe step types, their option schemas, which have side effects, the limits, and the one permission this API takes
2. CreatePOST /workflowId, revision, trigger._id, and a webhook key if a webhook trigger was requested
3. Add stepsPOST /{workflowId}/nodesA nodeId per step and a new revision each time
4. BranchPOST /{workflowId}/nodes/{nodeId}/branchesA branch index, then steps placed with position.branchIndex
5. CheckPOST /{workflowId}/validatewarnings, advisory: what a run may ignore or misread. No finding here refuses a deploy
6. TestPOST /{workflowId}/testA per-step trace: executed, simulated, skipped, or failed. Send mode: "dry-run" to keep it side-effect free
7. DeployPOST /{workflowId}/deploystatus: deployed and an armed trigger
8. FireThe trigger itself (a webhook call, an ingest batch, a schedule)A run
9. ReadGET /{workflowId}/runs, then GET /runs/{runId}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}.

Identifiers carried between calls

IdentifierComes fromUsed for
<API_KEY>A community API key holding workflowBuilderEvery call, as Authorization: Bearer <API_KEY>
workflowIdPOST / or GET /Every workflow-scoped path
nodeIdPOST /{workflowId}/nodesStep reads, changes, tests, deletes, moves and branch routes
trigger._idPOST / and PUT /{workflowId}/triggerA reference when a step reads the trigger output
revisionEvery response that returns a workflow or a stepIf-Match on the next write, when you want a concurrent change refused
runIdGET /{workflowId}/runsGET /runs/{runId} and POST /runs/{runId}/retry
Webhook keyThe POST / or PUT /{workflowId}/trigger response that minted itThe 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.

What does not change either way:

  • 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.
  • 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 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 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, 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.
  • 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.
  • 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.

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 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 is matched against the community's real fields, not refused. A read action names total_xp or total_coins, a write action names xp or coin, and a custom field names its own key. A typo is stored, POST /{workflowId}/validate reports UNKNOWN_USER_FIELD as a warning, and the step reads or writes nothing at run time.
  • A read step's result is keyed by the field's label. Send the label you want to reference, or omit it and take the catalog's, for example Total XP. A later step references that label, and validation reports REFERENCE_UNKNOWN_FIELD when it names one the step does not produce.

Shared error envelope

Every error is ordinary JSON with the same shape, whatever the status. 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 shows both. Every other error looks like this:

Response
{
  "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: 400 for a request the API cannot interpret, 401 for a credential problem, 403 for the permission the key does not hold, 404 for an object that does not exist in this community, 409 for a conflict, 413 for a body too large to read (meta.code is VALIDATION_FAILED), 415 for a body that is not declared JSON, 429 when the community's API rate limit is reached (meta.code is RATE_LIMITED), 500 for an unexpected failure and 503 for a write that was not attempted. There is no 422 and no 428: nothing refuses a deploy on validation, and If-Match is optional.

Next steps

Endpoints on this page