> ## 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

> The orchestration layer that turns a Recommend API request into ranked, personalized offers.

<Note>
  **See also**: [Decision Flows REST API reference](/api-reference/decision-flows) for request/response shapes, status codes, and error semantics.
</Note>

A **Decision Flow** is the brain of KaireonAI. Every time you need to decide *which offer to show a customer*, a Decision Flow runs behind the scenes. It loads the eligible inventory, pulls in customer context, filters out anything that should not be shown, scores what remains using ML models, and returns a ranked list of personalized recommendations — all in under 200ms.

Think of it as a pipeline with a clear contract: a customer walks in one end, and the best offers come out the other.

<Info>
  Decision Flows are executed through the [Recommend API](/api-reference/recommend). Pass a `decisionFlowId` in the request body and the engine does the rest.
</Info>

***

## How It Works

Decision Flows use a composable pipeline with **16 node types** organized across **3 phases**. You assemble a pipeline by choosing which nodes to include and configuring each through a visual canvas editor.

```
Phase 1 (Narrow) --> Phase 2 (Score & Rank) --> Phase 3 (Output)
```

| Phase                 | Purpose                                                                                                    | Node Types                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **1 -- Narrow**       | Load candidates, enrich with customer data, filter by rules and policies                                   | `inventory`, `match_creatives`, `enrich`, `qualify`, `contact_policy`, `filter`, `conditional`, `call_flow` |
| **2 -- Score & Rank** | Score each candidate, then either rank (single-placement top-N) or group into placements (multi-placement) | `score`, `optimize`, `rank`, `group`                                                                        |
| **3 -- Output**       | Compute personalized values and format the response                                                        | `compute`, `set_properties`, `response`                                                                     |
| **Cross-phase**       | `call_flow` may sit in Phase 1 or 2; `extension_point` defaults to Phase 1 but can be placed in any phase  | `call_flow`, `extension_point`                                                                              |

<Note>
  Two structural rules the validator enforces at save time:

  * The **`optimize` node is deprecated** — at runtime it passes scores through unchanged. Configure multi-objective weighting with `strategyProfileId` on the Score node instead.
  * **`rank` and `group` are mutually exclusive** — a flow may contain a `rank` node **or** a `group` node, not both (`RANK_AND_GROUP_CONFLICT`). Use `rank` for single-placement top-N, or `group` for multi-placement allocation.
</Note>

For a full reference of all node types, phase rules, and configuration options, see the [Composable Pipeline](/data/transforms/composable-pipeline) page.

For an operator-facing reference of every configurable field on every node — including how each knob is consumed by the engine and how to verify it had an effect — see [Node Configuration Reference](/decisioning/node-configuration-reference). For the operator-facing decision guide on the Score node's three methods (`priority_weighted`, `propensity`, `formula`) plus channel/strategy overrides and the propensity score floor, see [Scoring Strategies](/decisioning/scoring-strategies).

### Keyboard Shortcuts

| Shortcut             | Action                  |
| -------------------- | ----------------------- |
| Ctrl/Cmd + Z         | Undo last action        |
| Ctrl/Cmd + Shift + Z | Redo last undone action |

Undo/redo buttons are also available in the editor toolbar.

### Lifecycle & publication — draft vs active vs published

A flow has a `status` (`draft`, `active`, `paused`, `archived`) and a list of `publishedVersions[]`. The engine refuses to run a flow unless its status is `active` (or `published`) AND at least one published version exists. This is the **publish-or-refuse gate** — drift between "still building" and "live in production" is intentionally hard to cross by accident.

* Edits land on `draftConfig` (live as you type).
* `POST /api/v1/decision-flows/publish` snapshots `draftConfig` into `publishedVersions[]` as a new versioned entry with optional `notes`.
* The engine reads the latest entry of `publishedVersions[]` — not `draftConfig` — when resolving the route. Save without publishing → engine still serves the old version.
* `paused`, `archived`, and `draft` (or any status with no published version) are refused. Recommend calls against them throw `Decision flow "<key>" is not in a runnable state (status="...", publishedVersions=...)`, unless `previewDraft` is set.

The Studio's **Recommendation Preview** panel needs an escape hatch to test draft changes before publishing. It sets `previewDraft: true` in the engine context, which suspends the gate for that one call. This is studio-only — the public `POST /api/v1/recommend` endpoint never accepts the flag from the network.

### The decision context (auto-assembled)

At the start of every Recommend call, the engine assembles **one canonical, entity-namespaced decision context** for the customer — automatically, before any node in the flow runs. Gates, compute formulas, and scoring predictors all read from this single flat record. Its namespaces:

| Namespace      | Source                                                                                                                                                                                                                                                                                                                                                                                                                         | Example keys                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| `customer.*`   | The tenant's base `customer` schema row (`entityType: customer`)                                                                                                                                                                                                                                                                                                                                                               | `customer.credit_score`, `customer.tier` |
| `<entity>.*`   | Each active [Schema Join](/api-reference/schema-joins) with `autoEnrich: true`. The prefix is the joined schema's **slugified name** (lowercased, non-alphanumerics collapsed to `_` — "Retail Customers" → `retail_customers`). One-to-many joins are rolled up via the join's `aggregations` (e.g. `accounts.balance_sum`); a join with no aggregations configured gets `<entity>.count` plus the first row's scalar fields. | `accounts.balance_sum`, `accounts.count` |
| `behavior.*`   | The customer's [Behavioral Metric](/studio/behavioral-metrics) values, keyed by the **slugified metric name** (e.g. the metric "Converts 30d" → `behavior.converts_30d`). Customer-level values; dimensioned metrics are rolled up across dimensions. These are the same stored MetricValue rows the `metric_condition` gate reads — never recomputed at decision time.                                                        | `behavior.converts_30d`                  |
| `journey.*`    | The customer's active [Journey](/studio/journeys) enrollment: `journey.current` (journey name, most recently active), `journey.step` (current step id), and `journey.enrolled` (array of `{journey, step}`, present only when enrolled in more than one journey). Absent — not an error — when the customer is not enrolled.                                                                                                   | `journey.current`, `journey.step`        |
| `attributes.*` | Request-time attributes from the Recommend body                                                                                                                                                                                                                                                                                                                                                                                | `attributes.tier`                        |

Assembly is resilient: a broken join, a failed metric load, or a journey-state error never fails the decision — that piece is omitted with a logged warning and the pipeline continues.

**Previewing the namespace:** [`GET /api/v1/decision-context/preview`](/api-reference/decision-context-preview) returns the full key catalog available for authoring — every `customer.*` column, each active join's rollup keys (and its raw `<entity>[]` collection), every active `behavior.*` metric, the `journey.*` keys, and the documented `attributes.*` entries, each with its type and source. Pass `?flowKey=` to apply a specific flow's `excludeJoinIds[]` opt-outs. This is what the Decisioning Gates attribute picker autocompletes from.

<Note>
  **Enrichment is automatic — the Enrich node is an override.** You do not need an Enrich node for `customer.*`, `<entity>.*`, `behavior.*`, or `journey.*` to be available. Add an Enrich node only to *override or extend* the assembled context: attach a schema that isn't auto-joined, use a custom prefix / lookup key / field subset, or exclude specific joins. Enrich-node output wins over the auto-assembled context on key conflicts.
</Note>

In the Studio, the Enrich-node panel reflects this automatically: it opens with a green "Auto-enriched from data layer" card listing every schema covered by an active join, and the explicit-source picker tags any duplicate schema with "· auto-joined" plus an amber notice. See the [Schema Joins API](/api-reference/schema-joins) for how to define joins.

**Per-flow exclusion (#168):** auto-enrichment is tenant-wide, but a single flow can opt out of specific joins by adding their `id` to `EnrichNodeConfig.excludeJoinIds[]`. The Enrich-node panel exposes this with an `×` button on each green auto-join chip — click it and the chip becomes struck-through, indicating that join is skipped for this flow only. Other flows are unaffected. Use this for flows that don't need a specific join's data (saves a request-time lookup), or where you want a manual Enrich source to fully replace an auto-join rather than just override its fields. The original behavior (zero exclusions) remains the default. When a flow has multiple Enrich nodes, their `excludeJoinIds` are unioned.

<Warning>
  **Deprecated: unscoped `<rawschemaname>.*` keys.** Before the canonical context, auto-enriched fields were exposed under the raw (lowercased) schema name and base-customer fields under the raw schema name verbatim. These legacy alias keys are still emitted during the transition so existing gates and formulas keep resolving — but the canonical `customer.*` / `<entity>.*` form always wins on conflicts, each legacy-only alias logs a deprecation warning, and the aliases will be removed after the migration window. Re-author gates, compute formulas, and predictors against the canonical namespaces.
</Warning>

### Implicit Contact Policy

The engine **auto-applies all active Contact Policy rules** (DNC, frequency caps, cooldowns, suppression windows) at the end of Phase 1, even when the flow has no explicit `contact_policy` node wired in. This is intentional — Contact Policies are safety rails that protect against compliance breaches (e.g. forgetting to enforce a do-not-contact list). The opt-out is the flow-level `skipContactPolicy: true` flag, reserved for flows that must legitimately bypass these rails (transactional notifications, OTPs, system-of-record updates).

How the engine decides which path to take:

| Flow shape                                                      | Engine behavior                                                                             |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| No `contact_policy` node, `skipContactPolicy = false` (default) | Engine injects an implicit `contact_policy` node with `mode = "all"` at the end of Phase 1. |
| Explicit `contact_policy` node present (any `mode`)             | Engine respects the explicit node's config. No additional implicit injection.               |
| No `contact_policy` node, `skipContactPolicy = true`            | Engine skips contact policy entirely. Use only when the flow must bypass safety rails.      |

If you want a flow to run *some* policies (e.g. only DNC, skip frequency caps), add an explicit `contact_policy` node with `mode = "selected"` and pick the specific `contactPolicyIds[]`. The implicit injection happens *only* when no explicit node is present.

### Retail Rewards Example

A retail rewards Decision Flow might work like this:

1. **Inventory** -- Load all 10 retail rewards offers with their 60 creatives
2. **Enrich** -- Look up this customer's purchase history, reward tier, and visit frequency
3. **Qualify** -- Drop "Buy 5 Get 1 Free" for customers who already redeemed it this month
4. **Contact Policy** -- Suppress email offers for customers who received 3 emails this week
5. **Score** -- Run a Bayesian model to predict which offers this customer is most likely to engage with
6. **Rank** -- Sort by PRIE score, return the top 3

***

## PRIE Ranking Formula

The **PRIE formula** is KaireonAI's multiplicative scoring model. It produces a single priority score from four dimensions:

```
Priority = P x R x I x E
```

The multiplicative structure means **a zero in any dimension eliminates the candidate entirely**. This prevents irrelevant high-value offers from surfacing and ensures all four dimensions must be satisfied.

### The Four Factors

| Factor | Name       | Range | Source                                        | Set By                                   |
| ------ | ---------- | ----- | --------------------------------------------- | ---------------------------------------- |
| **P**  | Propensity | 0--1  | ML model prediction                           | Scoring engine (per offer-customer pair) |
| **R**  | Relevance  | 0--1  | Channel match, recency, segment overlap       | Computed automatically                   |
| **I**  | Impact     | 0--1  | `offer.businessValue` (0--100, normalized)    | Business user (per offer)                |
| **E**  | Emphasis   | 0--2  | `offer.priority` (0--100, normalized, scaled) | Marketer (per offer)                     |

<Info>
  **Impact vs. Emphasis:** Impact (`businessValue`) captures objective business value — how much is the offer worth if the customer converts. Emphasis (`priority`) is a subjective lever for marketers to boost or suppress offers based on campaign strategy. See [Offers -- Business Value](/studio/offers#priority-and-business-value) for details.
</Info>

### Two-Layer Architecture

1. **Flow-level weights** -- Configured on the Score node, these control how much each factor matters relative to the others. Weights must sum to 1.0. They are global tuning knobs for your decisioning strategy.
2. **Per-offer factor values** -- Each offer provides its own values for each dimension. These are the raw inputs that get weighted and multiplied together.

### Worked Example

Two retail rewards offers scored for the same customer, with flow-level weights `P=0.4, R=0.2, I=0.2, E=0.2`:

| Factor               | BOGO Iced Beverage                    | Earn 3x Points                        |
| -------------------- | ------------------------------------- | ------------------------------------- |
| P (model score)      | 0.85                                  | 0.60                                  |
| R (context)          | 0.70                                  | 0.90                                  |
| I (businessValue=80) | 0.80                                  | 0.40                                  |
| E (priority=70)      | 0.70                                  | 0.90                                  |
| **PRIE score**       | **0.85 x 0.70 x 0.80 x 0.70 = 0.333** | **0.60 x 0.90 x 0.40 x 0.90 = 0.194** |

BOGO Iced Beverage wins despite lower relevance and emphasis, because its higher propensity and business value outweigh the other factors.

<Info>
  When `explain=true` is passed to the [Recommend API](/api-reference/recommend), each decision in the response includes a `rankingScores` object with the individual PRIE factor values (`propensity`, `relevance`, `impact`, `emphasis`) and the final `composite` score. This makes it easy to debug why one offer outranked another without needing to reconstruct the math manually.
</Info>

***

## Score Node Configuration

The Score node supports three methods — `priority_weighted`, `propensity`, and `formula`. **PRIE is the `formula` method** — the multiplicative model described above — and it exposes four weight sliders (P, R, I, E). The other two are simpler strategies; see [Scoring Strategies](/decisioning/scoring-strategies) for when to use each.

| Setting                 | Description                                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Model selector**      | Choose a model for the Propensity factor. Leave empty for priority-based scoring (P defaults to `offer.priority / 100`). |
| **PRIE weight sliders** | Adjust relative importance of each factor. Must sum to 1.0.                                                              |
| **Overrides**           | Scope-specific overrides for different models and weights.                                                               |

### Overrides

You can add overrides scoped to a composite **offer\_channel**, **category\_channel**, or **subcategory\_channel** tuple, or to a single axis — **offer**, **category**, **subcategory**, or **channel**. Each override can specify a different model AND custom PRIE weights.

| Override Scope        | Key format                    | Example use case                                                                                                                                           |
| --------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `offer_channel`       | `<offerId>:<channelId>`       | Use a specialized model only for *this* offer on *this* channel — per-(action, channel) propensity granularity, native (no composition required)           |
| `category_channel`    | `<categoryId>:<channelId>`    | Bind one model to a whole category on one channel — e.g. "model A for retention offers on email" — without enumerating per-offer `offer_channel` overrides |
| `subcategory_channel` | `<subCategoryId>:<channelId>` | Same as `category_channel` at subcategory granularity                                                                                                      |
| `offer`               | `<offerId>`                   | Specialized model for a high-value offer regardless of channel                                                                                             |
| `category`            | `<categoryId>`                | Different PRIE weights for beverages vs. merchandise                                                                                                       |
| `subcategory`         | `<subcategoryId>`             | Boost emphasis weight for seasonal items                                                                                                                   |
| `channel`             | `<channelId>`                 | Lower propensity weight for email where model accuracy is lower                                                                                            |

### Override priority

The default priority order is `offer_channel → category_channel → subcategory_channel → offer → category → subcategory → channel → default`. Composite (two-axis) scopes rank above their single-axis components — a `category_channel` override beats a plain `category` override for candidates on the matching channel. The most-specific match wins; the resolver short-circuits on the first hit. Operators can customize via `score.overridePriority: ["offer_channel", "category_channel", ..., "default"]` — the order in the array is the order checked.

<Note>
  A matching override always wins over the node's static `defaultModel` / `modelKey` (and over a legacy `channelOverrides` entry). Previously the overrides ladder only ran when no `defaultModel` was configured; overrides now take precedence exactly as the priority chain documents.
</Note>

### Channel Score Overrides

The Score node supports a `channelOverrides` array for entirely different scoring strategies per channel:

```json theme={null}
{
  "scoring": {
    "modelKey": "default-scorecard",
    "weights": { "P": 0.3, "R": 0.3, "I": 0.2, "E": 0.2 },
    "channelOverrides": [
      {
        "channelId": "channel_web",
        "method": "scorecard",
        "modelKey": "web-scorecard-v2",
        "weights": { "P": 0.4, "R": 0.2, "I": 0.2, "E": 0.2 }
      },
      {
        "channelId": "channel_mobile",
        "method": "bayesian",
        "modelKey": "mobile-bayesian-v1",
        "weights": { "P": 0.6, "R": 0.1, "I": 0.2, "E": 0.1 }
      }
    ]
  }
}
```

The score-config resolver checks the candidate's channel against the overrides array. First match wins; no match falls back to the default config.

***

## Extension Points

Extension Points let you inject custom logic at three critical moments in the pipeline without modifying the core flow. They appear as **dashed-border placeholder nodes** on the canvas and are **no-op when unconfigured**.

| Hook             | Phase          | When It Fires                  | Use Cases                                                         |
| ---------------- | -------------- | ------------------------------ | ----------------------------------------------------------------- |
| `pre_score`      | Before scoring | After enrichment and filtering | External API enrichment, last-minute filters, real-time features  |
| `score_override` | During scoring | After built-in scorer runs     | Replace/adjust scores with an external model, champion-challenger |
| `post_rank`      | After ranking  | After ranking, before response | Budget caps, diversity constraints, portfolio guardrails          |

Each extension point can link to a **sub-flow** (a separate Decision Flow that runs inline):

```json theme={null}
{
  "type": "extension_point",
  "hookName": "pre_score",
  "configured": true,
  "subFlowId": "df_custom_enrichment"
}
```

<Tip>
  Extension points are the recommended way to integrate external ML models for score adjustment. Use `score_override` to call your model and blend or replace the built-in score.
</Tip>

***

## Ranking Influencers

**Ranking Influencers** create a feedback loop between past customer outcomes and future scoring. When a customer has positive interactions with offers in a category, other offers in that same category receive a score boost. Negative outcomes lead to a demotion.

### How It Works

1. **Load interaction history** -- Query the customer's all-time interaction summaries (up to 50 most recent), map each offer to its category
2. **Compute category affinity** -- `boost = (positiveRate - negativeRate) * 0.1`, clamped to -0.1 to +0.1 (max 10% adjustment)
3. **Apply to scores** -- `adjustedScore = score * (1 + categoryBoost)`

### Example

A customer has interacted with 3 retail rewards beverage offers: 2 positive, 0 negative.

```
positiveRate = 2/3 = 0.667, negativeRate = 0/3 = 0.000
boost = (0.667 - 0.000) * 0.1 = 0.067 (6.7%)
```

All other beverage offers for this customer get a 6.7% score boost.

| Setting                     | Type    | Default | Description                                     |
| --------------------------- | ------- | ------- | ----------------------------------------------- |
| `rankingInfluencersEnabled` | boolean | `true`  | Enable/disable category-based score adjustments |

<Tip>
  Disable influencers during initial deployment or A/B testing if you want a clean baseline without feedback effects.
</Tip>

***

## Control Groups

KaireonAI supports an **always-on control group** for measuring the true incremental lift of your Decision Flows. A configurable percentage of Recommend API calls receive randomized scores instead of model-driven scores, creating a baseline for lift measurement.

### How It Works

1. **Deterministic assignment** -- Hash of `customerId + date` decides group membership. Same customer, same day, same group.
2. **Random scoring** -- Control group requests still run the full pipeline (enrichment, qualification, contact policies), but scores are randomized.
3. **Response flag** -- The response includes `controlGroup: true | false` for downstream analytics segmentation.

| Setting               | Type    | Range | Default | Description                                        |
| --------------------- | ------- | ----- | ------- | -------------------------------------------------- |
| `controlGroupPercent` | integer | 0--10 | 2       | Percentage of Recommend calls in the control group |

<Info>
  A 2% default means roughly 2 out of every 100 calls get randomized scores — enough for statistically meaningful lift measurement without materially impacting business outcomes.
</Info>

***

## NBA Kill Switch

The **NBA Kill Switch** is a tenant-level safety control that instantly disables Decision Flow execution across your entire tenant.

**Setting:** `nbaEnabled` (boolean, default `true`) in **Settings > General**.

When `nbaEnabled = false`:

* Recommend API bypasses all Decision Flow execution
* Offers are returned ranked by `priority` only (simple ordering)
* No enrichment, scoring, filtering, or contact policy evaluation occurs
* Response includes `"mode": "fallback"` flag

<Warning>
  The kill switch is for emergency use — for example, if a flow misconfiguration is causing production errors. It does not disable the Recommend API itself; it only simplifies ranking to a safe fallback.
</Warning>

***

## Auto-Routing

KaireonAI reduces manual wiring by automatically creating **FlowRoutes** when you add channels and placements.

* **First flow created** is automatically marked as the default
* **New channel** -- FlowRoute created to the default flow
* **New placement** -- FlowRoute created to the default flow

New channels and placements are immediately functional without manual routing setup. Override any auto-created route by editing it in the Decision Flow configuration or the Channel detail page.

<Info>
  Auto-routing only creates routes to the **default** flow. To route a channel to a different flow, create the route manually or change the default flow first.
</Info>

***

## Auto-Assembly

When `autoAssembly` is enabled (the default), the flow's inventory updates automatically as your catalog changes. Creating a new offer, activating a creative, or adding a decisioning gate triggers reassembly.

**Assembly triggers:** Offers (created/activated/archived), Creatives (created/linked), Channels, Sub-categories, Decisioning gates, Contact policies.

Each trigger is logged in the flow's `assemblyLog` with a timestamp, source entity, and changes applied. Set `autoAssembly: false` for full manual control.

***

## Decision Traces

Decision traces are forensic records that answer: *"Why did customer X get Offer Y instead of Offer Z?"*

### Configuration

| Setting                   | Description                                           |
| ------------------------- | ----------------------------------------------------- |
| `decisionTraceEnabled`    | Master toggle (boolean)                               |
| `decisionTraceSampleRate` | Percentage of requests that generate a trace (0--100) |

Configure in **Settings > General > Retention > Decision Trace**.

### What a Trace Captures

| Field                  | Always | With `debug: true`     |
| ---------------------- | ------ | ---------------------- |
| `totalCandidates`      | Yes    | Yes                    |
| `afterQualification`   | Yes    | Yes                    |
| `afterContactPolicy`   | Yes    | Yes                    |
| `topScores`            | Yes    | Yes                    |
| `afterConsent`         | --     | Yes                    |
| `afterGuardrails`      | --     | Yes                    |
| `qualificationReasons` | --     | Yes (per-offer detail) |
| `contactPolicyReasons` | --     | Yes (per-offer detail) |
| `guardrailReasons`     | --     | Yes (per-offer detail) |

`afterGuardrails` is the real candidate count after the
[guardrail stage](/api-reference/guardrails#enforcement-at-decision-time)
removes any `hard`-blocked candidates. It runs once per request at rank-node
entry (or the response node when a flow has no rank node), so ranking and the
result `limit` operate on the survivors. `guardrailReasons[]` lists each failed
guardrail evaluation as `{ ruleKey, ruleName, severity, passed, reason?, offerId? }`.

<Tip>
  Use 100% sample rate during development. In production, 1--5% keeps storage costs manageable while giving enough data to investigate issues.
</Tip>

***

## Outbound Batch Decisioning

For outbound campaigns (email, direct mail), process an entire customer segment in one API call:

```bash theme={null}
POST /api/v1/recommend/batch
```

```json theme={null}
{
  "decisionFlowKey": "spring-campaign",
  "segmentId": "seg_high_value",
  "channel": "email",
  "limit": 3,
  "outputFormat": "json"
}
```

| Field             | Type    | Required | Default  | Description                 |
| ----------------- | ------- | -------- | -------- | --------------------------- |
| `decisionFlowKey` | string  | Yes      | --       | Decision Flow to execute    |
| `segmentId`       | string  | Yes      | --       | Customer segment to process |
| `channel`         | string  | No       | --       | Filter by channel type      |
| `limit`           | integer | No       | 3        | Max offers per customer     |
| `outputFormat`    | enum    | No       | `"json"` | `json` or `csv`             |

The response includes per-customer recommendations plus an aggregate summary with `avgOffersPerCustomer`, `topOffers`, and `categoryDistribution`.

<Info>
  Batch decisioning runs the same pipeline as real-time Recommend — enrichment, qualification, contact policies, scoring, and ranking all apply. The only difference is iteration over a segment instead of a single customer.
</Info>

***

## Portfolio Optimization

<Warning>
  The standalone **Optimize node is deprecated**. At runtime it is a passthrough — `case "optimize"` in `pipeline-runner.ts` logs a deprecation notice and leaves every candidate's score unchanged. Multi-objective weighting now lives on the **Score node**.
</Warning>

When you need to balance competing business objectives (revenue vs. customer experience vs. margin), attach a ranking (strategy) profile to the Score node via `strategyProfileId` — or a per-scope `strategyOverrides[]` entry keyed by category / productType / channel. The profile's objective weights are mapped onto the PRIE factors (e.g. `conversion → propensity`, `margin → impact`), replacing the inline `formula` weights when set. For details on creating profiles, see [Algorithms & Models](/ai-ml/algorithms).

***

## Worked Example: Full Pipeline

Walk through a complete Recommend API call to see how each stage transforms the candidate list.

**Setup:** 5 active offers (`offer-A` through `offer-E`), 1 Decision Flow, customer `C-4821` with `credit_score = 745`, PRIE scoring with a scorecard model, ranking: `topN`, `maxCandidates = 2`.

| Stage              | What Happens                                                                      | Candidates                 |
| ------------------ | --------------------------------------------------------------------------------- | -------------------------- |
| **Inventory**      | Load all 5 offers with creatives                                                  | 5                          |
| **Enrichment**     | Query `customers` table: `credit_score=745`, `income=92000`, `region="northeast"` | 5                          |
| **Qualification**  | `offer-D` fails `income >= 100000` rule                                           | 4                          |
| **Contact Policy** | `offer-C` (email-only) hits 3/week frequency cap                                  | 3                          |
| **Scoring**        | Scorecard model: offer-E=0.910, offer-A=0.820, offer-B=0.543                      | 3                          |
| **Ranking**        | Top 2 by score                                                                    | **2: \[offer-E, offer-A]** |

### Trace Output

```json theme={null}
{
  "traceSummary": {
    "totalCandidates": 5,
    "afterQualification": 4,
    "afterContactPolicy": 3,
    "topScores": [
      { "offerId": "offer-E", "score": 0.910 },
      { "offerId": "offer-A", "score": 0.820 }
    ]
  }
}
```

***

## Field Reference

### Create (POST /api/v1/decision-flows)

| Field          | Type    | Required | Default   | Description                             |
| -------------- | ------- | -------- | --------- | --------------------------------------- |
| `key`          | string  | Yes      | --        | Unique identifier (1--255 chars)        |
| `name`         | string  | Yes      | --        | Human-readable name (1--255 chars)      |
| `description`  | string  | No       | `""`      | Optional description                    |
| `status`       | enum    | No       | `"draft"` | `draft`, `active`, `paused`, `archived` |
| `autoAssembly` | boolean | No       | `true`    | Auto-update when catalog changes        |

### Update (PUT /api/v1/decision-flows)

| Field          | Type    | Required | Description                                       |
| -------------- | ------- | -------- | ------------------------------------------------- |
| `id`           | string  | Yes      | Flow ID to update                                 |
| `name`         | string  | No       | Updated name                                      |
| `description`  | string  | No       | Updated description                               |
| `status`       | enum    | No       | Updated status                                    |
| `autoAssembly` | boolean | No       | Updated auto-assembly                             |
| `draftConfig`  | object  | No       | Pipeline configuration (validated against schema) |
| `rowVersion`   | integer | No       | Optimistic concurrency control                    |

### Delete (DELETE /api/v1/decision-flows?id=...)

Soft-deletes the flow (retains record with `deletedAt` timestamp). Response: `{ "success": true, "cascaded": 0 }`.

***

## API Quick Reference

```bash theme={null}
POST /api/v1/decision-flows     # Create
GET  /api/v1/decision-flows     # List (paginated)
PUT  /api/v1/decision-flows     # Update (returns 409 on rowVersion mismatch)
DELETE /api/v1/decision-flows?id=df_abc123  # Soft-delete
```

For complete request/response schemas, see the [Decision Flows API Reference](/api-reference/decision-flows).

***

## Related

<CardGroup cols={3}>
  <Card title="Composable Pipeline" icon="puzzle-piece" href="/data/transforms/composable-pipeline">
    Build flows from 16 modular node types across 3 phases.
  </Card>

  <Card title="Algorithms & Models" icon="brain" href="/ai-ml/algorithms">
    Scoring engines, experiments, and portfolio optimization profiles.
  </Card>

  <Card title="Decisioning Gates" icon="shield-check" href="/decisioning/qualification-rules">
    Define eligibility gates that control which offers reach each customer.
  </Card>

  <Card title="Contact Policies" icon="clock" href="/decisioning/contact-policies">
    Frequency caps, cooldowns, and suppression rules.
  </Card>

  <Card title="Computed Values" icon="calculator" href="/tutorials/computed-values">
    Formula syntax, supported functions, and variable namespaces.
  </Card>

  <Card title="Glossary" icon="book" href="/reference/glossary">
    Definitions for Offer, Creative, Decision Flow, and other key terms.
  </Card>
</CardGroup>
