> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blujay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflows

> List the manual workflows in your workspace and enroll contacts in them, with safe retries.

Workflows are automated message sequences you build in Blujay. A workflow that starts manually can be started for a contact from the API, so an event in your own system, such as a signup or a missed appointment, can put someone into a Blujay sequence. You find the workflows with `GET /v1/workflows` and enroll a contact with `POST /v1/workflows/{workflow_id}/enrollments`.

## List workflows

`GET /v1/workflows` returns the active, published workflows in your workspace that start manually, oldest first, up to 100. Each has a `workflow_id` and a `label`. Workflows that start another way, such as when a tag is added, aren't listed. The list isn't paginated.

```bash curl theme={null}
curl https://api.blujay.io/v1/workflows \
  -H "Authorization: Bearer blu_your_api_key"
```

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "workflows": [
      { "workflow_id": "workflow_abc123", "label": "New customer welcome" },
      { "workflow_id": "workflow_def456", "label": "Appointment reminder" }
    ]
  },
  "trace_id": "trc_8b0d2f4a6c1e",
  "timestamp": "2026-10-14T17:50:00.000Z"
}
```

Workflow steps and run history aren't returned. Build and edit workflows in the Blujay app.

## Enroll a contact

Send the contact's ID and a `request_id` that identifies this enrollment in your system.

```bash curl theme={null}
curl https://api.blujay.io/v1/workflows/workflow_abc123/enrollments \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "contact_abc123",
    "request_id": "signup-A1042",
    "metadata": { "source": "signup_form", "external_id": "A-1042" }
  }'
```

```json Response (201) theme={null}
{
  "status": "success",
  "data": {
    "workflow_id": "workflow_abc123",
    "contact_id": "contact_abc123",
    "request_id": "signup-A1042",
    "enrollment_status": "run_created",
    "workflow_run": { "status": "active" }
  },
  "trace_id": "trc_2c4e6a8b0d1f",
  "timestamp": "2026-10-14T17:51:00.000Z"
}
```

A `201` means the contact was enrolled and a workflow run started. The workflow's first step runs from Blujay's queue after the response; the messages it sends show up as `message.sent` and later events on your webhooks, with no link back to the enrollment.

### Response fields

| Field | Values | Meaning |
| - | - | - |
| `enrollment_status` | `run_created`, `no_op` | Whether this request started a run. |
| `no_op_reason` | `contact_opted_out`, `duplicate_non_terminal_run` | Present when `enrollment_status` is `no_op`. See below. |
| `workflow_run.status` | `active`, `waiting`, `paused`, `blocked`, `completed`, `failed`, `exited` | The run's state at the time of the response. A new run is `active`. For `duplicate_non_terminal_run` it is the existing run's state: `active`, `waiting`, `paused` or `blocked`. `workflow_run` is `null` when no run exists. |
| `workflow_run.status_reason_code` | string | Present when the run's state has a reason, for example `phone_number_unavailable`, `sender_retired`, `sender_suspended`, `workflow_interrupted`, `unsupported_workflow_step` or `internal_error`. Other reasons pass through from the workflow engine as written. |

## Retry safely with request\_id

`request_id` is required (1 to 200 characters) and is how this endpoint avoids double enrollments. It doesn't use the `Idempotency-Key` header.

* Sending the same `request_id` for the same workflow and contact again returns `200` with the original result. The contact isn't enrolled twice.
* The original result wins: a repeat with different `metadata` doesn't change the enrollment.
* The same `request_id` for a different contact is a separate enrollment.
* A request that was rejected with an error isn't remembered. After you fix the problem, send it again with the same `request_id`.

Use an ID that is stable for the event you're reacting to, such as your signup ID or order ID, so a retry after a timeout reuses it. Use a new ID when you want to enroll the same contact again later. See [Idempotency](/idempotency.md) for how other endpoints handle retries.

## Understand enrollments that don't start a run

A `200` can also mean Blujay accepted the enrollment but didn't start a new run. Check `enrollment_status` and `no_op_reason`:

| `no_op_reason` | Meaning | `workflow_run` |
| - | - | - |
| `contact_opted_out` | The contact has opted out of messages, on the contact itself or by an opt-out of their phone number or email. See [Opt-outs](/opt-outs.md). | `null` |
| `duplicate_non_terminal_run` | The contact is already in a run of this workflow that hasn't finished. | The existing run |

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "workflow_id": "workflow_abc123",
    "contact_id": "contact_abc123",
    "request_id": "signup-A1043",
    "enrollment_status": "no_op",
    "no_op_reason": "contact_opted_out",
    "workflow_run": null
  },
  "trace_id": "trc_4e6a8c0b2d1f",
  "timestamp": "2026-10-14T17:52:00.000Z"
}
```

## Add metadata

`metadata` is optional context about the enrollment, such as where it came from. Blujay stores it on the enrollment record so the enrollment can be traced back to your system when you contact [support](/support.md). Workflow steps can't read it, it doesn't change what the workflow sends, and no endpoint or webhook returns it.

* Up to 20 keys, each 1 to 64 characters.
* Values are strings (up to 500 characters), numbers, booleans or `null`.
* 4,096 bytes in total, measured as JSON.

Metadata over these limits returns `400` [`VALIDATION_ERROR`](/errors/validation-error).

## Handle errors

| Error | Status | What to do |
| - | - | - |
| [`NOT_FOUND`](/errors/not-found) | 404 | The workflow or contact doesn't exist in your workspace. |
| [`WORKFLOW_LIFECYCLE_ERROR`](/errors/workflow-lifecycle-error) | 409 | The workflow can't accept enrollments right now: it isn't active, or it doesn't start manually. Not retryable. Check it in Blujay, or pick another workflow from `GET /v1/workflows`, then send the enrollment again. The same `request_id` is fine. |
| [`WORKFLOW_VALIDATION_ERROR`](/errors/workflow-validation-error) | 422 | The workflow isn't set up to run, for example it has no published version or its phone number isn't available. Fix the workflow in Blujay. |
| [`VALIDATION_ERROR`](/errors/validation-error) | 400 | The request body is invalid, for example `request_id` is missing or `metadata` is too large. |

## Limitations

* Enrollment is fire-and-forget. There is no endpoint to list a contact's runs, read a run's progress, cancel a run, or remove a contact from a workflow, and no webhook when a run starts, finishes or fails. Manage runs in the Blujay app.
* Only workflows that start manually can be enrolled through the API. Tag-triggered workflows start when you [add the tag](/contacts.md#add-a-tag).
* One run per contact per workflow at a time: a second enrollment returns `no_op` with `duplicate_non_terminal_run` until the first run ends.
* An opted-out contact is never enrolled (`no_op`, `contact_opted_out`).
* The messages a workflow sends follow the same [sending rules](/messaging/sending-rules.md) and plan limits as API sends. A run on a Free Plan or AI Agent number can only message people who texted that number first; see [Plans](/plans.md).
* `metadata` is write-only.

## Next steps

> **[Contacts](/contacts.md)**
>
> Create or find the contact to enroll.

> **[Opt-outs](/opt-outs.md)**
>
> Understand why an opted-out contact isn't enrolled.

> **[Idempotency](/idempotency.md)**
>
> See how retries work across the API.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.