Skip to main content

AI decisioning

AI decisioning answers one question per profile: given the actions you could take right now, which one is most likely to convert? You pass the candidate actions — each a message variant on a channel, optionally pinned to a send hour, with its observed results so far — and the decision endpoint returns the best pick plus every candidate’s score. This guide covers what the engine decides, what you feed it, how to read the output, the dashboard tester, and when decisioning is the right tool versus a campaign-triggered flow or a plain segment. The same endpoint backs the dashboard page at Audience → Decisioning. The UI and the API run the same decision.

1. What AI decisioning is

Each call to POST /api/v1/cdp/decisioning/decide makes one decision for one profile: which action to take next. An action is the combination of three things:
  • Variant — which message variant to send (subject_a, winback_v2).
  • Channel — where to send it (email, SMS, WhatsApp, push).
  • Send hour — when to send it, as a local hour 0–23 (optional; an action with no hour is available at any time).
The engine scores every candidate action with a conversion bandit and returns the winner. Because it balances exploiting the current best performer against exploring under-sampled options, the pick keeps improving as each send’s outcome feeds the next call’s counts. Three decisioning siblings answer different questions — pick the one that matches yours: Offer decisioning solves eligibility — active windows, per-contact caps, budgets, exclusivity groups, segment and cart gates — and can hold out a control cohort so you measure incremental ROI. Use it when the question is “which promotion”, and AI decisioning when the question is “which variant, on which channel, at what time”. Recommendations ranks a product catalog, not message actions (see Decision output vs. the recommendations list).

2. Inputs the engine consumes

A decision request carries three kinds of input, all resolved by you before the call — the engine itself is stateless and stores nothing:
  1. Candidate actions (variants, 1–50 per call) — each with a variant_id, a channel, an optional send_time_hour, and the evidence that action has accumulated: trials (sends or impressions) and successes (conversions). Build the candidates from your campaign setup; roll up the counts from your own event data — a conversion is an event you already track in the CDP, so the counts come out of your segments, computed traits, and event rollups rather than a new stored model.
  2. Eligibility gates — an action marked eligible: false is never chosen, and an eligible_channels list restricts the decision to channels the profile can actually receive on. Resolve consent and frequency-cap status for the profile first (the decisioning tester in the dashboard does this automatically — see the walkthrough below), then pass the allowlist so a suppressed channel is never the winning action.
  3. Decision context — a target_send_hour (when you intend to send; actions pinned to a different hour drop out of contention), a policy (bayes_ucb default, ucb1, or epsilon_greedy), and optional tuning knobs (epsilon, exploration_z, prior_alpha, prior_beta). Most callers leave all of these at the defaults.
Decisioning runs after your audiences and scores exist — it decides over actions for the profiles you already identified, it does not identify them. Reach it from the segments you built in CDP audiences and the propensity scores from CDP predictive models; segment membership tells you who to contact, decisioning tells you how.

3. Decision output vs. the recommendations list

The response gives you the pick and the transparency to audit it:
  • chosen — the action to take, or null when nothing is eligible (for example a profile opted out on every channel). Always handle null: the correct action then is to not send.
  • explored — true when the bandit passed over the current best performer to gather evidence on a less-proven action. Expect occasional explore picks; that is how the engine discovers a new winner instead of locking onto an early leader.
  • scores — the full ranking with each action’s estimated conversion rate (posterior_mean) and ranking score, so you can see why the winner won and what it beat.
  • eligible_count / total_trials — how many actions were genuinely in contention and how much evidence the decision rested on.
All three policies rank on the same evidence — conversions per trial — and differ only in how they trade exploitation against exploration. bayes_ucb (the default) is deterministic and reproducible; ucb1 forces untried actions first; epsilon_greedy explores a fixed share of the time. How this differs from POST /api/v1/cdp/recommendations/rank: recommendations answers “which products” — it ranks a product catalog by a profile’s affinities and returns a list (a top-N) with a per-factor breakdown, for personalizing a catalog or product grid. AI decisioning answers “which action” — it picks one winner per profile across variant, channel, and timing, and returns the audit breakdown around that single pick. A typical flow uses both: decide the action and channel here, then fill the message with ranked products from recommendations.

4. Dashboard walkthrough

Open Audience → Decisioning to run a decision interactively. The tester posts to the same endpoint, so what you see is what an integration gets.
  1. Define the candidate actions. Each row is one action: a variant id, a channel from the channel picker, an optional send hour, and the trials and successes observed so far. Add or remove rows to match your campaign’s variants.
  2. Choose the policy. The policy selector defaults to bayes_ucb; the advanced fields (epsilon, exploration_z, priors) map to the optional request parameters and can usually stay blank.
  3. Narrow by profile and timing (optional). Enter a Profile id to resolve a real contact: the tester reads that profile’s consent and frequency-cap status and automatically holds out actions on channels the contact opted out of or where a frequency cap is exhausted — each exclusion is labelled with its reason on the result card. Set a Target send hour to decide for a specific hour; actions pinned to a different hour are excluded from the pick.
  4. Run the decision. The result shows the chosen action, whether the pick was an exploration, and the full scored table. When you set no target hour, the send-hour schedule panel shows the best action per send hour — the same timing gate the engine applies for a target hour, run across every candidate hour — so you can read a full day’s schedule in one view.
The page is role-gated in the UI, mirroring the API: only owner, admin, and developer roles can run decisions.

5. API usage

Decide the next-best action for one profile:
With target_send_hour: 18, subject_a (pinned to hour 9) is still scored and reported but cannot be chosen; the pick falls between the hour-18 email variant and the any-time SMS variant.

The feedback loop — record the outcome, fold it into the next call

Nothing travels back automatically: you decide, you send, you observe the conversion, and you increment the counts yourself. The loop for one profile:
  1. Decide. The response’s chosen carries the trials/successes the winner had at decision time.
  2. Render and send. Read chosen.variant_id and chosen.channel, build the message for that variant, and submit it through your campaign. When the response’s chosen is null, skip the send — the profile is out of contention on every eligible channel.
  3. Fold the result back. Store the decision alongside its counts: base_trials = chosen.trials, base_successes = chosen.successes. When the conversion arrives, your next call passes trials: base_trials + 1 and successes: base_successes + 1; when it never converts, only trials moves.
That is the whole contract — the endpoint is stateless, so each call is scored on the counts you hand it, and slipping a stale base in lets an old decision shadow a newer one. One base per profile at a time keeps the loop honest.
If the profile opted out of SMS between decisions, drop sms from eligible_channels — excluded actions still appear in scores with eligible: false, so the audit trail survives the suppression.

Sibling endpoints — runnable samples

The decision table at the top of this guide names two siblings; both are the same stateless, envelope-shaped kind of call as decisioning — decide, then act.

Offer decisioning — POST /api/v1/cdp/offers/decide

The response has the same shape as the decision barrel — data carries chosen, holdout, holdout_bucket (a deterministic 0–99 bucket per salt), plus evaluations, one row per candidate with eligible: false and ineligible_reason so you can audit why it was dropped.

Recommendations — POST /api/v1/cdp/recommendations/rank

The response is the envelope you already saw: data.recommendations sorted by score, each row with a per-factor factors breakdown (category / brand / tag / price_fit / popularity), plus weights. An experiment with per-variant weights returns deterministic A/B bucketing and echoes the assigned variant back. Feed it the decision the loop picked, and let it fill in the products for the chosen channel.

Errors and rate limits

Validation failures land on the standard VALIDATION_ERROR envelope, and the call surfaces it inline. The 422 is the shape a malformed body gets regardless of route, and the same envelope applies to the sibling endpoints. Two cases matter on an integrations page:
  • Persisted variants semantics — the engine rejects a body without any actions, a channel outside the string bounds, or an eligible_channels entry that is blank or longer than 64 characters. In every such case the 422 body below.
  • eligible_channels: [] — passing an empty allowlist does not make the request invalid; it narrows the decision set to zero. The call still returns 200 with chosen: null and eligible_count: 0; handle that branch before sending, which is what the feedback loop above already teaches.
422 Error
The 60/min/tenant limit returns the global limiter envelope with RATE_LIMITED plus Retry-After:
Retry-after handling, per Error-handling examples — prefer the server’s retry_after (header or envelope) over your own doubling:
For the full request/response schema, see the CDP API reference.

6. When to use what

A composed example: build a churn-risk audience with the predictive models, let decisioning pick the variant/channel/hour per member, hold the chosen variant out when a frequency cap blocks the channel, and send through your campaign.

See also