# Create or replace the trigger

Creates the workflow's single trigger, or replaces the existing one with the type and options you send.

- Endpoint: `PUT https://api.returning.ai/v1/workflow-builder/data-workflows/{workflowId}/trigger`
- Section: Workflow Builder / Trigger
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `workflowBuilder`
- Retries: A PUT with the same body is safe only while the revision is unchanged; a retry against a changed workflow is refused with `409 VERSION_CONFLICT` and writes nothing.
- Guide: generated from the published specification
- Last updated: 28 Oct 2026
- Web page: https://docs.returning.ai/api-reference/workflow-builder/create-or-replace-the-trigger

## 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 change applies as the last write. On a deployed workflow the new trigger takes effect for the next event, immediately, with no extra header.

| Failure | Meaning | Recovery |
| --- | --- | --- |
| `400 VALIDATION_FAILED` | Unknown trigger type, or options that do not match the type | Read the trigger schema from `GET /catalog` and resend. |

## Behaviour

A workflow has exactly one trigger, so this is an upsert rather than an add.

A webhook trigger gets a generated key, and the response that mints it is the only response that shows the key; every later read replaces it with the redaction marker. There is no rotation: a `PUT` on an existing webhook trigger keeps the stored key and ignores an `apiKey.value` you send. Replacing an ingest or schedule trigger keeps that trigger's identity.

## 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. |
| `Content-Type` | `string` | Yes | Request body format. (`application/json`) |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `string` | Yes | Trigger type. (`time`, `webhook`, `visit-community`, `join-community`, `custom-field-update`, `conditions-trigger`, `event-ingest`) |
| `name` | `string` | No | Display name. Defaults to `Trigger`. (Max 200 chars) |
| `options` | `object` | No | Trigger options, validated against the trigger type's schema from `GET /catalog`. (Nullable) |

### Watch for

- `apiKey.enabled: true` on a trigger that has no stored key asks the platform to mint one. On a trigger that already has a key, the stored value is kept and any `value` you send is ignored; an empty string is refused.
- To get a different key, remove the webhook type first: set the trigger to another type, then set `webhook` again. No key is stored in between, so the second call mints a new one and shows it once. The trigger id is kept, but this is a replacement of the trigger's authentication, not a rotation, and the sender must be reconfigured.
- `tokenExchangeEnabled` and `apiKey.enabled` are booleans and are never redacted. They tell you the authentication mode without exposing the credential.
- To keep a stored key while changing something else, send the redaction marker back at that path. A marker at a path with no stored value is `400 REDACTED_VALUE_WITHOUT_STORED_SECRET`.
- The ingest batch id path is read from the root of the delivered event. A per-row batch id is not found, and the ingest fails before it runs.
- Record id paths are independent checks. A repeat of any one of them marks the row a duplicate, and the path name is not part of the stored identity.
- A trigger type that takes no options is stored with `options: null`. Send `null`, not an empty object.
- Changing the trigger type on a deployed workflow changes what fires it from the next event, immediately. There is no second version of the workflow, so a sender configured for the old type simply stops producing runs.

### Example request

```bash
curl --request PUT \
  --url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/trigger' \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "type": "webhook",
    "name": "Trade bonus trigger",
    "options": {
      "tokenExchangeEnabled": false,
      "apiKey": {
        "enabled": true
      }
    }
  }'
```

### More examples

An ingest trigger with a batch id path and per-row record id paths:

```bash
curl --request PUT \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/trigger" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{"type":"event-ingest","name":"Trades","options":{"batchIdPath":"metadata.batch_id","recordIdPaths":["trade_id","order_id"]}}'
```

A webhook trigger whose key the platform mints:

```bash
curl --request PUT \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/trigger" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{"type":"webhook","name":"Trade bonus trigger","options":{"tokenExchangeEnabled":false,"apiKey":{"enabled":true}}}'
```

A daily schedule:

```bash
curl --request PUT \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/trigger" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{"type":"time","name":"Every night","options":{"type":"day","time":2,"timezone":"UTC"}}'
```

The response that mints a webhook key:

```json
{
  "data": {
    "_id": "65f1a2b3c4d5e6f708192a3b",
    "communityId": "65f1a2b3c4d5e6f708192a50",
    "name": "VIP bonus",
    "status": "draft",
    "outgoingWebhookUrl": null,
    "createdAt": "2026-10-24T09:15:00.000Z",
    "updatedAt": "2026-10-24T12:20:02.000Z",
    "trigger": {
      "_id": "65f1a2b3c4d5e6f708192a41",
      "name": "Trade bonus trigger",
      "type": "webhook",
      "status": "idle",
      "options": { "tokenExchangeEnabled": false, "apiKey": { "enabled": true, "value": "<WEBHOOK_KEY>" } }
    },
    "nodes": [],
    "revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501",
    "redactedPaths": []
  }
}
```

## Response

HTTP `200` with the whole workflow and its new `revision`. A readback is the response itself, plus one more `GET /{workflowId}` to confirm the key is now redacted and the type and options are stored as sent.

`trigger._id` is what step options reference when a step reads the trigger output. It stays the same when the options change and changes only if the trigger is recreated.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `data` | `object` | - | A workflow with its trigger, its steps in canvas order and its revision. |
| `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__"]`. |

### Example response (200)

```json
{
  "data": {
    "_id": "65f1a2b3c4d5e6f708192a3b",
    "communityId": "65f1a2b3c4d5e6f708192a50",
    "name": "VIP bonus",
    "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": true,
          "value": "<WEBHOOK_KEY>"
        }
      }
    },
    "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": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c",
    "redactedPaths": []
  }
}
```

## Errors

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `VALIDATION_FAILED` | Unknown trigger type, options that do not match the type, or a batch id path that is not a string. Fix the named field. |
| 400 | `REDACTED_VALUE_WITHOUT_STORED_SECRET` | A marker arrived where nothing is stored, for example a copied export posted as a new trigger. Send the real value, or set the trigger again without the marker. |
| 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. |
| 413 | `VALIDATION_FAILED` | The request body is too large to read. Nothing was written. Send a smaller body; a large import is better split into a create plus step writes. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | The body was not declared `application/json`. Send the header. |

### 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 resend. |
| 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 trigger before retrying. A retry of the same PUT cannot mint a second key, because the stored key is kept. |
| 503 | `LOCK_UNAVAILABLE` | The write lock store did not answer, so nothing was written. Retry shortly. |

**Retries:** A PUT with the same body is safe only while the revision is unchanged; a retry against a changed workflow is refused with `409 VERSION_CONFLICT` and writes nothing. A retry after an ambiguous failure is the risky case: if the first call minted a webhook key, it was shown only in that lost response. Reading the workflow afterwards shows `["__REDACTED__"]`, and sending a value does not replace it. A lost key is not recoverable by retrying; plan for the replacement path below.

## Next step

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