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

# Decision Flows API

> Create, update, publish, and delete Decision Flows — the core decisioning pipelines.

<Note>
  **See also**: [Decision Flows concept and configuration](/decisioning/decision-flows) for what this API powers, when to call it, and how it is configured.
</Note>

<Frame caption="The Decision Flows page.">
  <img src="https://mintcdn.com/kaireonai/l-jsUQlUEuA3B6hG/images/screenshots/decision-flows-list.png?fit=max&auto=format&n=l-jsUQlUEuA3B6hG&q=85&s=d5abc231dda18ee0ad09d0534789d15b" alt="Decision Flows list view in the Kaireon Studio" width="1440" height="900" data-path="images/screenshots/decision-flows-list.png" />
</Frame>

Decision Flows are the heart of KaireonAI's decisioning engine. Each flow defines a composable pipeline of nodes (enrichment, qualification, scoring, ranking, filtering) that selects and ranks Offers for a given customer context.

<Info>
  See the [Decision Flows feature page](/decisioning/decision-flows) and [Composable Pipeline reference](/data/transforms/composable-pipeline) for UI guidance and architecture details.
</Info>

## Base path

```
/api/v1/decision-flows
```

***

## List Decision Flows

```
GET /api/v1/decision-flows
```

Returns a paginated list of Decision Flows, ordered by status then by last update (newest first).

### Query parameters

| Parameter        | Required | Type    | Description                                                                          |
| ---------------- | -------- | ------- | ------------------------------------------------------------------------------------ |
| `limit`          | No       | integer | Maximum results per page. Default `50`, max `100`.                                   |
| `cursor`         | No       | string  | Cursor for keyset pagination (an `id`; returns rows with `id` less than the cursor). |
| `includeDeleted` | No       | boolean | When `true`, includes soft-deleted flows.                                            |

<Note>
  If the tenant has zero Decision Flows, this endpoint lazily auto-creates a base flow and returns it, so the list is never empty for an active tenant.
</Note>

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "df_001",
      "tenantId": "t_001",
      "key": "credit-card-nba",
      "name": "Credit Card NBA",
      "description": "Next-best-action flow for credit card offers.",
      "status": "active",
      "autoAssembly": true,
      "isDefault": true,
      "isProtected": false,
      "couplingOverride": null,
      "draftConfig": { "version": 2, "nodes": [] },
      "publishedVersions": [
        { "version": 1, "publishedAt": "2026-03-12T10:00:00.000Z", "notes": "Initial publish" }
      ],
      "rowVersion": 3,
      "createdAt": "2026-03-10T12:00:00.000Z",
      "updatedAt": "2026-03-14T15:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "cursor": null,
    "hasMore": false,
    "total": 2
  }
}
```

***

## Get a single Decision Flow

```
GET /api/v1/decision-flows/{id}
```

Returns one Decision Flow by ID for the authenticated tenant. Soft-deleted flows return `404`.

### Path parameters

| Parameter | Required | Type   | Description       |
| --------- | -------- | ------ | ----------------- |
| `id`      | **Yes**  | string | Decision Flow ID. |

### Response `200`

```json theme={null}
{
  "id": "df_001",
  "tenantId": "t_001",
  "key": "credit-card-nba",
  "name": "Credit Card NBA",
  "description": "Next-best-action flow for credit card offers.",
  "status": "active",
  "autoAssembly": true,
  "draftConfig": { "version": 2, "nodes": [] },
  "publishedVersions": [
    { "version": 1, "publishedAt": "2026-03-12T10:00:00.000Z", "notes": "Initial publish" }
  ],
  "rowVersion": 3,
  "createdAt": "2026-03-10T12:00:00.000Z",
  "updatedAt": "2026-03-14T15:00:00.000Z"
}
```

### Error codes

| Code  | Reason                                              |
| ----- | --------------------------------------------------- |
| `401` | Missing or invalid auth.                            |
| `404` | Flow not found, soft-deleted, or in another tenant. |

### Roles

admin, editor, viewer.

***

## Create a Decision Flow

```
POST /api/v1/decision-flows
```

Creates a new Decision Flow with a default draft configuration.

### Request body

| Field               | Required | Type                                | Description                                                                                                                                                                                                                                                                                                          |
| ------------------- | -------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`               | **Yes**  | string (1-255)                      | Unique machine-readable key (used in the Recommend API).                                                                                                                                                                                                                                                             |
| `name`              | **Yes**  | string (1-255)                      | Human-readable name.                                                                                                                                                                                                                                                                                                 |
| `description`       | No       | string                              | Flow description.                                                                                                                                                                                                                                                                                                    |
| `status`            | No       | enum                                | `draft` (default), `active`, `paused`, `archived`.                                                                                                                                                                                                                                                                   |
| `autoAssembly`      | No       | boolean                             | Auto-assemble when offers/channels change. Default `true`.                                                                                                                                                                                                                                                           |
| `couplingOverride`  | No       | `"partial"` \| `"atomic"` \| `null` | Per-flow override of the channel's `couplingMode`. Resolution at decision time: `flow.couplingOverride > channel.couplingMode > "partial"`. Use this when a single channel serves both atomic flows (e.g. email weekly digest) and partial flows (e.g. email transactional). `null` (default) defers to the channel. |
| `skipContactPolicy` | No       | boolean                             | Opt out of implicit Contact Policy. Default `false` — the engine auto-applies global Contact Policy rules (DNC, frequency caps, cooldowns) when the flow has no explicit `contact_policy` node. Set `true` only for flows that must bypass safety rails (transactional, OTP, etc.).                                  |
| `draftConfig`       | No       | object                              | Initial pipeline configuration (V2 schema). If omitted, the flow is created with a minimal default `{ version: 2, nodes: [], flowConfig: {} }`. When provided it is validated structurally before write.                                                                                                             |

<Note>
  `isDefault` is **not** a settable request field on create — it is stripped by the validation schema. The **first** flow created in a tenant is automatically marked default; subsequent flows are created with `isDefault: false`.
</Note>

### Example request

```json theme={null}
{
  "key": "credit-card-nba",
  "name": "Credit Card NBA",
  "description": "Next-best-action flow for credit card offers.",
  "autoAssembly": true
}
```

### Response `201`

Returns the created Decision Flow. The `draftConfig` is populated with the default stage configuration and `publishedVersions` starts as an empty array.

### Error codes

| Code  | Reason                                                                                                               |
| ----- | -------------------------------------------------------------------------------------------------------------------- |
| `400` | Validation error (missing key or name).                                                                              |
| `403` | Insufficient role (requires `editor` or `admin`).                                                                    |
| `409` | A Decision Flow with that key already exists.                                                                        |
| `413` | Request body exceeds the 2 MB limit.                                                                                 |
| `415` | `Content-Type` is not `application/json`.                                                                            |
| `422` | The provided `draftConfig` failed schema or pipeline validation (e.g. a `rank` and a `group` node in the same flow). |
| `429` | Rate limit exceeded (500 requests / 60 s).                                                                           |

***

## Update a Decision Flow

```
PUT /api/v1/decision-flows
```

Updates an existing Decision Flow. When `draftConfig` is provided, it is validated against the pipeline schema and goes through the pipeline validator for structural correctness.

### Request body

| Field              | Required | Type                                | Description                                                                                                                                                                 |
| ------------------ | -------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`               | **Yes**  | string                              | Decision Flow ID to update.                                                                                                                                                 |
| `name`             | No       | string                              | Updated name.                                                                                                                                                               |
| `description`      | No       | string                              | Updated description.                                                                                                                                                        |
| `status`           | No       | enum                                | `draft`, `active`, `paused`, `archived`.                                                                                                                                    |
| `autoAssembly`     | No       | boolean                             | Toggle auto-assembly.                                                                                                                                                       |
| `isProtected`      | No       | boolean                             | Protected flows cannot be deleted (auto-set to `true` on publish).                                                                                                          |
| `isDefault`        | No       | boolean                             | Mark this flow as the tenant default. Setting `true` unsets `isDefault` on every other flow in the tenant.                                                                  |
| `couplingOverride` | No       | `"partial"` \| `"atomic"` \| `null` | Per-flow override of the channel's `couplingMode`. See create body table above for resolution order. Send `null` to clear the override and fall back to the channel's mode. |
| `draftConfig`      | No       | object                              | Updated pipeline configuration.                                                                                                                                             |
| `rowVersion`       | No       | integer                             | Optimistic concurrency control. If provided and it does not match the current `rowVersion`, the update is rejected with `409`.                                              |

<Warning>
  Always send `rowVersion` when updating `draftConfig` to prevent overwriting concurrent edits. On conflict, the API returns `409` with a message to refresh and retry.
</Warning>

### Flow config v2 schema

The `draftConfig` must conform to the flow config v2 schema:

* `version`: Must be `2`.
* `nodes`: Array of pipeline nodes (minimum 2). Each node has `id`, `type`, and `config`.
* `flowConfig`: Optional flow-level configuration.

### Pipeline node types

16 node types organized across 3 phases:

| Phase                    | Node Type         | Description                                                             |
| ------------------------ | ----------------- | ----------------------------------------------------------------------- |
| **Phase 1 (Narrow)**     | `inventory`       | Load candidate offers from the catalog                                  |
|                          | `match_creatives` | Match offers to creatives for the request channel                       |
|                          | `enrich`          | Load customer data from schema tables                                   |
|                          | `qualify`         | Evaluate decisioning gates                                              |
|                          | `contact_policy`  | Apply contact policy suppression rules                                  |
|                          | `filter`          | Generic filter node                                                     |
|                          | `conditional`     | Conditional branching based on expressions                              |
| **Phase 2 (Score/Rank)** | `score`           | Score candidates using PRIE formula                                     |
|                          | `optimize`        | Multi-objective portfolio optimization                                  |
|                          | `rank`            | Rank and select top candidates                                          |
|                          | `group`           | Group candidates by placement or category                               |
| **Phase 3 (Output)**     | `compute`         | Evaluate formula-based personalized values                              |
|                          | `set_properties`  | Set custom properties on candidates                                     |
|                          | `response`        | Format the final response                                               |
| **Cross-phase**          | `call_flow`       | Execute a sub-flow inline                                               |
|                          | `extension_point` | Inject custom logic at pre\_score, score\_override, or post\_rank hooks |

<Warning>
  **`rank` and `group` are mutually exclusive.** A flow may contain a `rank` node (single-placement top-N selection) **or** a `group` node (multi-placement allocation across zones), but not both — `rank` throttles `group`'s input and leaves placements unfilled. A `draftConfig` containing both fails structural validation with `RANK_AND_GROUP_CONFLICT` and the create/update request is rejected with `422`.
</Warning>

### Score node methods

The Score node uses the PRIE formula (Propensity x Relevance x Impact x Emphasis). Scoring methods include:

* `priority_weighted` -- Uses offer priority and business value
* `propensity` -- Uses a scoring model for the P factor
* `formula` -- Custom formula-based scoring

### Rank node methods

| Method            | Description                                  |
| ----------------- | -------------------------------------------- |
| `topN`            | Select the top N candidates by score         |
| `diversity`       | Ensure category/channel diversity in results |
| `round_robin`     | Rotate across categories or channels         |
| `explore_exploit` | Balance known-good offers with exploration   |

### Compute node

Supports `formulaExtras` for adding personalized computed values to the response. Each extra defines a `key`, `formula`, and `outputType` that are evaluated per candidate at decision time.

### Example request

```json theme={null}
{
  "id": "df_001",
  "draftConfig": {
    "version": 2,
    "nodes": [
      { "id": "n1", "type": "inventory", "config": { "scope": "category", "categoryIds": ["cat_01"] } },
      { "id": "n2", "type": "qualify", "config": {} },
      { "id": "n3", "type": "score", "config": { "method": "priority_weighted" } },
      { "id": "n4", "type": "rank", "config": { "method": "topN", "topN": 5 } },
      { "id": "n5", "type": "response", "config": {} }
    ]
  },
  "rowVersion": 2
}
```

### Response `200`

Returns the updated Decision Flow. The `rowVersion` is incremented.

### Error codes

| Code  | Reason                                                   |
| ----- | -------------------------------------------------------- |
| `400` | Validation error on `draftConfig`.                       |
| `404` | Decision Flow not found.                                 |
| `409` | `rowVersion` mismatch (concurrent edit) or key conflict. |
| `422` | Pipeline validation failed (structural errors).          |

***

## Delete a Decision Flow

```
DELETE /api/v1/decision-flows?id={flowId}
```

Soft-deletes a Decision Flow by ID. The record is marked as deleted but retained in the database for audit purposes.

### Query parameters

| Parameter | Required | Type   | Description                 |
| --------- | -------- | ------ | --------------------------- |
| `id`      | **Yes**  | string | Decision Flow ID to delete. |

### Response `200`

```json theme={null}
{
  "success": true,
  "cascaded": 0
}
```

The `cascaded` field indicates how many related records (if any) were also soft-deleted.

<Note>
  This endpoint uses **soft-delete** -- the record is not physically removed. It is excluded from GET results by default. To include soft-deleted records, pass `?includeDeleted=true` on the GET request.
</Note>

### Error codes

| Code  | Reason                                               |
| ----- | ---------------------------------------------------- |
| `400` | Missing `id` query parameter, or soft-delete failed. |
| `401` | Missing or invalid auth.                             |
| `403` | Insufficient role (requires `editor` or `admin`).    |
| `404` | No Decision Flow with that `id` in your tenant.      |

***

## Publish a Decision Flow

```
POST /api/v1/decision-flows/publish
```

Snapshots the current `draftConfig` as a new published version. The flow status is set to `active`. A scoring method must be configured before publishing.

### Request body

| Field   | Required | Type   | Description                     |
| ------- | -------- | ------ | ------------------------------- |
| `id`    | **Yes**  | string | Decision Flow ID to publish.    |
| `notes` | No       | string | Release notes for this version. |

### Example request

```json theme={null}
{
  "id": "df_001",
  "notes": "Added propensity scoring stage"
}
```

### Response `200`

Returns the updated Decision Flow with the new version appended to `publishedVersions`.

```json theme={null}
{
  "id": "df_001",
  "status": "active",
  "publishedVersions": [
    { "version": 1, "publishedAt": "2026-03-12T10:00:00.000Z", "notes": "Initial publish", "configSnapshot": {} },
    { "version": 2, "publishedAt": "2026-03-14T15:00:00.000Z", "notes": "Added propensity scoring stage", "configSnapshot": {} }
  ],
  "rowVersion": 4
}
```

### Four-eyes publish approval (opt-in)

When the tenant setting `requirePublishApproval` is `true`, publish is gated:
the flow must have a **fresh, approved** [ApprovalRequest](/api-reference/approvals)
with `entityType: "decisionFlow"`, `action: "publish"`, and `entityId` equal to
the flow's id. Because approval-stage walking already rejects self-approval and
duplicate approvers, an approved request implies two distinct identities.

One approval authorizes **exactly one** publish: the successful publish stamps
the approval's `approvalId` onto the `publishedVersions[]` entry it creates, so a
second publish on the same approval is rejected as `consumed`. The next publish
needs a new approval.

The gate **fails closed** — if the tenant-settings lookup errors, publish is
blocked unless a valid approval already exists. The default
(`requirePublishApproval: false`) keeps one-click publish for existing tenants.

A blocked publish returns `422` and writes a `publish_blocked` audit entry:

```json theme={null}
{
  "title": "Publish approval required",
  "detail": "This tenant requires four-eyes approval before publishing decision flows. ...",
  "reason": "no_approval"
}
```

`reason` is `"no_approval"` (no fresh approved request) or `"consumed"` (the most
recent approval was already used by a previous publish). The audit `reason` is
`publish_approval_missing` or `publish_approval_consumed` respectively.

### Error codes

| Code  | Reason                                                                                                                                                                                                                                                                                                    |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404` | Decision Flow not found.                                                                                                                                                                                                                                                                                  |
| `409` | Concurrent modification detected. Retry.                                                                                                                                                                                                                                                                  |
| `422` | The draft has no scoring config (a `scoring.method` for a V1 draft, or a non-empty `nodes` array for a V2 draft), **or** the opt-in fairness hard gate detected a disparate-impact breach over the last 7 days of traces, **or** four-eyes publish approval is required and missing/consumed (see above). |

***

## Role requirements

| Method         | Minimum role |
| -------------- | ------------ |
| GET            | `viewer`     |
| POST (create)  | `editor`     |
| PUT            | `editor`     |
| DELETE         | `editor`     |
| POST (publish) | `editor`     |

***

## Optimistic concurrency

Decision Flows support optimistic concurrency control via the `rowVersion` field. When updating a flow:

1. Read the current `rowVersion` from the GET response.
2. Include `rowVersion` in your PUT request body.
3. If the server's `rowVersion` does not match, the update is rejected with `409 Conflict` and a message to refresh and retry.
4. On successful update, the `rowVersion` is automatically incremented.

This prevents concurrent editors from overwriting each other's changes.

***

## Soft-delete and audit

Decision Flows use **soft-delete** with audit snapshots. When a flow is deleted:

1. The `deletedAt` timestamp is set (record is retained).
2. An audit snapshot is captured with the full state before deletion.

Updates also create audit snapshots via `auditedUpdate`, incrementing the `rowVersion` on each change. The first flow created in a tenant is automatically marked as the default (`isDefault: true`).

To include soft-deleted flows in GET responses, add `?includeDeleted=true` to the query string.

<Card title="Decision Flows" icon="diagram-project" href="/decisioning/decision-flows">
  Learn more about building Decision Flows in the platform UI.
</Card>
