Skip to main content

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.

Tutorial: Winback

Winback campaigns target customers who’ve gone quiet — they had a relationship, they stopped, and the question is whether a well-timed offer can revive them. The flow has to know when each customer went silent, what they did before, and what offer might bring them back without over-messaging the lapsed-and-uninterested. Business scenario: an e-commerce platform’s data shows a long tail of customers who haven’t purchased in 90+ days. A blanket discount email goes to most of them; the platform wants to instead route lapsed-but-recoverable customers to a personalized offer matched to their prior purchase category — and route the truly lost customers to no message at all (don’t waste cost reaching out). What you’ll build:
  • A Customer schema with lastPurchaseDate and lifetimePurchases derived fields
  • 3 winback offers per category (Apparel, Electronics, Home)
  • A flow that segments lapsed customers and skips the unrecoverable ones
  • Time-of-day routing so messages land during the customer’s local active window
  • A holdout group for measuring incremental winback
Prerequisites: A running KaireonAI instance, an admin API key. Time: 25–30 minutes.

1. Define the schema with lapse signals

curl -X POST $BASE/api/v1/schemas -d '{
  "name": "Customer",
  "fields": [
    { "name": "lastPurchaseDate",   "type": "date" },
    { "name": "lifetimePurchases",  "type": "integer" },
    { "name": "lifetimeRevenue",    "type": "decimal" },
    { "name": "primaryCategory",    "type": "string" },
    { "name": "preferredChannel",   "type": "string" },
    { "name": "timezone",           "type": "string" },
    { "name": "consentEmail",       "type": "boolean" },
    { "name": "consentSMS",         "type": "boolean" }
  ]
}'
The consent* fields are non-negotiable. Winback hits dormant customers; the platform must respect consent state down to the channel level.

2. Define computed fields for the lapse window

Computed fields run at decision time per-customer. We need to know how long the customer has been silent.
curl -X POST $BASE/api/v1/categories -d '{
  "name": "Apparel",
  "computedFields": [
    {
      "name": "daysSinceLastPurchase",
      "formula": "round((today() - customer.lastPurchaseDate) / 86400)",
      "outputType": "integer"
    },
    {
      "name": "isRecoverable",
      "formula": "lifetimePurchases >= 3 && lifetimeRevenue >= 50",
      "outputType": "boolean"
    }
  ]
}'
isRecoverable encodes “this customer was a real customer, not a one-time tire-kicker.” The flow will skip everyone where it’s false. See Computed Values for the formula-engine reference.

3. Define winback offers per category

# Apparel-category lapsed customer
curl -X POST $BASE/api/v1/offers -d '{
  "name": "Apparel — 20% back",
  "category": "Apparel",
  "priority": 70,
  "businessValue": 30,
  "margin": 50,
  "qualificationRules": [
    { "field": "primaryCategory", "op": "==", "value": "apparel" },
    { "field": "daysSinceLastPurchase", "op": ">=", "value": 90 },
    { "field": "daysSinceLastPurchase", "op": "<=",  "value": 365 },
    { "field": "isRecoverable", "op": "==", "value": true }
  ]
}'

# Electronics — different lever, higher margin
curl -X POST $BASE/api/v1/offers -d '{
  "name": "Electronics — free shipping + 10% off",
  "category": "Electronics",
  "priority": 75,
  "businessValue": 60,
  "margin": 35,
  "qualificationRules": [
    { "field": "primaryCategory", "op": "==", "value": "electronics" },
    { "field": "daysSinceLastPurchase", "op": ">=", "value": 90 },
    { "field": "daysSinceLastPurchase", "op": "<=",  "value": 365 },
    { "field": "isRecoverable", "op": "==", "value": true }
  ]
}'

# Home & kitchen
curl -X POST $BASE/api/v1/offers -d '{
  "name": "Home — bundle 3, save 15%",
  "category": "Home",
  "priority": 70,
  "businessValue": 40,
  "margin": 45,
  "qualificationRules": [
    { "field": "primaryCategory", "op": "==", "value": "home" },
    { "field": "daysSinceLastPurchase", "op": ">=", "value": 90 },
    { "field": "daysSinceLastPurchase", "op": "<=",  "value": 365 },
    { "field": "isRecoverable", "op": "==", "value": true }
  ]
}'
The 365-day upper bound is important: a customer who’s been gone 18 months is a different problem — they need win-back-from-cold, not a discount nudge. Different category, different flow.

4. Quiet hours via channel config

A winback email at 2am does worse than no email. Configure channel quiet-hours:
curl -X POST $BASE/api/v1/channels -d '{
  "name": "email",
  "config": {
    "quietHoursStart": "21:00",
    "quietHoursEnd":   "08:00",
    "timezoneSource":  "customer.timezone"
  }
}'

curl -X POST $BASE/api/v1/channels -d '{
  "name": "sms",
  "config": {
    "quietHoursStart": "20:00",
    "quietHoursEnd":   "09:00",
    "timezoneSource":  "customer.timezone"
  }
}'
The platform consults the customer’s timezone at request time and refuses to deliver if it’s within the quiet window. SMS quiet hours are wider because SMS is more intrusive.

5. Contact policy — cap winback sequence at 3 touches

curl -X POST $BASE/api/v1/contact-policies -d '{
  "name": "Winback 3-touch cap, 30-day window",
  "scope": "category:Apparel,Electronics,Home",
  "ruleType": "frequency_cap",
  "windowDays": 30,
  "maxImpressions": 3
}'
A 3-touch cap over 30 days lets the platform run a real sequence: initial → reminder → final-call. Beyond 3 the customer has signaled they’re not interested.

6. Wire the winback flow

curl -X POST $BASE/api/v1/decision-flows -d '{
  "name": "Winback",
  "config": {
    "nodes": [
      { "id": "inv",     "type": "inventory", "config": { "categories": ["Apparel","Electronics","Home"] } },
      { "id": "enr",     "type": "enrich",    "config": { "schema": "Customer" } },
      { "id": "compute", "type": "compute" },
      { "id": "qual",    "type": "qualify" },
      { "id": "cp",      "type": "contact_policy" },
      { "id": "score",   "type": "score",     "config": { "method": "propensity" } },
      { "id": "rank",    "type": "rank",      "config": {
          "profile": "winback",
          "weights": { "Wp": 0.30, "Wr": 0.20, "Wi": 0.20, "We": 0.05, "Wu": 0.25 }
      }},
      { "id": "out",     "type": "response" }
    ]
  }
}'
Wu: 0.25 is intentionally high. Winback is where the platform’s CATE signal earns its keep — half of the lapsed customers would have come back anyway (sure things) and the other half won’t come back regardless (lost causes). The narrow band of persuadable customers in the middle is the ROI of the whole program.

7. Recommend for a lapsed apparel customer

curl -X POST $BASE/api/v1/recommend -d '{
  "customerId": "cust_lapsed_1",
  "channel": "email",
  "flow": "Winback"
}'
Sample response:
{
  "decisions": [
    {
      "offerId": "off_apparel_20pct",
      "name": "Apparel — 20% back",
      "score": 0.71,
      "rank": 1,
      "trace": {
        "qualified": true,
        "computedFields": { "daysSinceLastPurchase": 142, "isRecoverable": true },
        "contactPolicy": "passed (0 contacts in last 30d)",
        "upliftSegment": "persuadable",
        "cate": 0.12
      }
    }
  ],
  "decisionId": "dec_winback_1",
  "latencyMs": 51
}
Same request at 2am customer-local time:
{
  "decisions": [],
  "reason": "channel email is within quiet hours (21:00-08:00 local) — deliver after 08:00",
  "decisionId": "dec_winback_2",
  "latencyMs": 38
}
The platform refused to deliver — not because the offer was wrong, but because the timing was. Operators see this in the trace; the calling system can retry at the next active window.

8. Measuring incremental winback

Winback campaigns famously measure poorly because returning customers would have returned anyway. Set up a real holdout:
curl -X POST $BASE/api/v1/experiments -d '{
  "name": "Winback Q4 holdout",
  "flowId": "flow_winback",
  "holdoutPercentage": 15,
  "primaryMetric": "revenue_per_customer",
  "duration": "90 days"
}'
After 90 days, the platform’s z-test endpoint computes the difference in revenue per customer between the 85% who received winback offers and the 15% holdout — and confidence-bounds the result so you can tell finance “winback drives Xincrementalrevenue,witha95X incremental revenue, with a 95% CI of [X-low, $X-high]”. This is the only honest way to claim winback ROI.

9. What’s next

  • Winback-from-cold flow. Customers gone > 365 days need a different program (re-acquisition price, full re-onboarding). Create a sibling flow scoped to daysSinceLastPurchase > 365.
  • Multi-step sequence. Chain initial email → no-open SMS reminder → final-call branded postcard via a Journey, with the contact policy ensuring each customer gets at most 3 touches.
  • Channel-preference learning. Track which channel each customer engages with and feed that back into PRIE’s R (relevance) so subsequent winback runs prefer the customer’s surface.
  • Segmentation by margin. Premium lapsed customers (high lifetime revenue) deserve a different offer (account-manager call) than commodity-lapsed customers (discount email). Split the flow on lifetimeRevenue thresholds.

See Cross-Sell for the existing-customer growth scenario, Churn Prevention for the retention scenario, or Computed Values for the formula-engine reference.