For a scheduled campaign, start with a batch decision job if that meets the business need. Use an online API when context, stock or eligibility changes during the customer journey. Both should share the same accounting, rule evaluation and logging logic.
The flow shows a new decision. A retry replays the saved decision and reuses or completes its idempotent reservation.
Diagram source
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","textColor":"#17324D","primaryColor":"#F5F7FA","primaryTextColor":"#17324D","primaryBorderColor":"#8796A8","lineColor":"#718096","secondaryColor":"#EFF6FF","tertiaryColor":"#E9F6F2","clusterBkg":"#F8FAFC","clusterBorder":"#D6DEE8","edgeLabelBackground":"#FFFFFF"},"flowchart":{"htmlLabels":true,"nodeSpacing":18,"rankSpacing":28,"curve":"linear","padding":10,"subGraphTitleMargin":{"top":8,"bottom":12}}}}%%
flowchart TB
accTitle: Offer decision serving flow
accDescr: Eligible requests pass through scoring, resource checks and durable logging before execution, with a no-offer fallback and mature-outcome review.
R["Request + available<br/>features"] --> E{"Eligible?"}
M["Score actions<br/>including no offer"] --> B{"Resources?"}
L["Persist decision<br/>+ probabilities"] --> D["Reserve + deliver once"]
D --> O["Mature profit<br/>+ guardrails"]
E -->|Yes| M
E -->|No| F["No offer fallback"]
M -->|Error| F
B -->|Yes| L
B -->|No| F
F --> L
classDef data fill:#EFF6FF,stroke:#3973AC,color:#17324D,stroke-width:1.5px;
classDef model fill:#EFF2FD,stroke:#596FC2,color:#17324D,stroke-width:1.5px;
classDef decision fill:#FFF5DF,stroke:#A8751A,color:#17324D,stroke-width:1.5px;
classDef execution fill:#E9F6F2,stroke:#23866E,color:#17324D,stroke-width:1.5px;
classDef neutral fill:#F5F7FA,stroke:#8796A8,color:#17324D,stroke-width:1.5px;
class R,L,O data;
class M model;
class E,B decision;
class D execution;
class F neutral;The eligibility gate checks eligibility, contact rules and input freshness. The resource gate checks budget and stock before the final action draw. Either gate failing, or a model error, selects no offer. On both paths, save the original assignment, chosen action and real post-constraint assignment probabilities. Then recheck resources and reserve budget, atomically and idempotently, before delivery. In this order, a crash during reservation or delivery still leaves a saved decision to recover.
If the execution recheck fails, append the no-offer result and reason, keeping the original assignment. Record assignment and execution separately. Assignment probabilities belong to the assigned action, not the one that overrode it. Mature outcomes feed review and controlled policy updates.
An example POST /recommend-offer request contains a customer/account ID, channel, decision time
and idempotency key. The response contains decision_id, action ID and catalogue version, expiry,
reason code and policy version. Keep debug estimates in restricted logs. Internal causal or model
terms need not appear in the offer copy. A repeated request with the same key must return the same
decision, reserving budget and sending a message only once.
Save the decision before delivery, and use a durable outbox or similar retry design. Record the candidate set, scores, real selection probabilities, assignment and any fallback. A sample log record might be:
{
"decision_id": "d_123",
"customer_id": "c_456",
"decision_at": "2026-10-08T06:00:00Z",
"feature_snapshot": "snapshot_abc",
"eligible_actions": ["no_offer", "reminder", "coupon_10"],
"selected_action": "coupon_10",
"action_probabilities": {"no_offer": 0.25, "reminder": 0.25, "coupon_10": 0.5},
"policy_version": "offer_policy_v3",
"catalog_version": "catalog_v7",
"experiment_id": "offer_collection_01",
"budget_reservation_id": "reservation_789",
"fallback_reason": null
}These probabilities show one collection policy, not a split to use everywhere. Log the real probabilities from the selection mechanism you built. In a deterministic policy, the chosen action has probability one and the others zero, which limits later evaluation. For a multi-item slate, log the joint or sequential selection probabilities your evaluation method needs. Item scores that look independent are not a slate propensity.
On a timeout, stale features, invalid output, exhausted budget or a failed stock check, return the defined safe rule-based decision or no offer. Do not reuse an expired coupon. Log the cause, and keep retry behaviour intact. Monitor input freshness, delivery, p95 latency, contact-limit and budget breaches, action mix, observed costs, outcome maturity, calibration, complaints and experimental incremental value. Drift alone is a reason to investigate, not an automatic reason to retrain.
Roll out in steps: log replay, shadow scoring, an A/A logging check where you can, and a limited randomised policy test. Increase traffic only once the predeclared criteria are met and outcomes have matured. Rollback restores the previous policy and rules, not just the model binary. Version the training cutoff, feature definitions, catalogue, calibration, constraints and policy so any decision can be rebuilt.