Skip to main content
KaireonAI authors offer rules in four ordered stages. All four are stored as qualification rule records with a stage discriminator. At decision time, Eligibility and Fit run as hard filters, Match applies a soft multiplier to each surviving offer’s score, and Ranking-stage rules are persisted for authoring but are not yet applied to ordering (see the note under Stages).

Stages

  1. Eligibility — Hard pass/fail filter. Legal, compliance, do-not-contact. Failure blocks the offer immediately.
  2. Fit Filters — Hard product-fit filter. Whether the offer could logically apply to this customer at all. A failing offer is dropped, exactly like Eligibility.
  3. Match Scoring — Soft multiplier. Given the offer is eligible and fits, match-stage rules produce a multiplier that is applied to the offer’s score (it does not drop the offer). This stage is functional.
  4. Ranking — Authoring stage for rules intended to adjust the final ordering. Not yet applied at decision time — see the note below.
At decision time, Eligibility and Fit are evaluated together as hard filters (a failing offer is removed), and Match is the soft multiplier that scales each surviving offer’s score. Ranking-stage rules are persisted and shown in the studio, but they do not currently affect offer ordering — neither the recommend runtime nor the batch executor reads stage-4 rules. Final ordering is governed by the ranking profile (Scoring Strategies), not by ranking-stage qualification rules.
All four stages are authored on the Decisioning Gates page in the studio sidebar. Ranking profiles (the weighting configuration that combines objectives) are still configured under Scoring Strategies, but the stage-4 rules themselves now live alongside the other gates.

Authoring rules

The studio surface at /studio/qualification-rules (label: Decisioning Gates) lists every rule in the tenant. A pill row at the top filters by stage:
  • All — every rule regardless of stage
  • Eligibility — hard gates only
  • Fit Filters — product-fit only
  • Match Scoring — soft-scoring multipliers only
  • Ranking — final-ordering rules only
Each rule row exposes the rule type, scope assignments, priority, and the stage classification. Click any row to edit; click + New Decisioning Rule to author a new one.

Selecting a stage

The rule editor surfaces a 4-button stage selector arranged in a 2×2 grid. Pick the stage that best matches the rule’s intent:
  • Use Eligibility for legal/compliance gates that block the offer entirely (hard filter).
  • Use Fit Filters for product-fit checks that should drop the offer when they fail (e.g. customer doesn’t already own the offer) — also a hard filter.
  • Use Match Scoring for soft scoring that multiplies the offer’s score (e.g. propensity threshold, recency boost). This is the only stage that scales rather than drops.
  • Use Ranking to author rules intended to adjust the final ordering after match scoring (e.g. campaign priority boosts, recency-bias adjustments). Note: ranking-stage rules are not yet read at decision time — see the note above.
The default rule type lookup adapts to the stage: segment_required, attribute_condition, metric_condition default to Eligibility; propensity_threshold, recency_check default to Match.

AI parse rule

The new-rule form exposes an AI parse rule button. Paste a natural-language description of the rule (e.g. “only customers in the US with balance over 10,000”) and the assistant returns a draft rule type, config payload, and stage. Review and edit before saving — the parser is a starting point, not the source of truth.

API

GET /api/v1/qualification-rules?stage=eligibility|fit|match|ranking filters by stage. Each returned row is enriched with a classifier-derived decisioningStage field so the UI can render the stage label without re-classifying client-side. POST /api/v1/qualification-rules accepts a body matching the create-qualification-rule request schema. The stage field is one of eligibility | fit | match | ranking; omit it to default to eligibility.

Migrating from legacy stage names

Tenants whose data was created before this rename may have rows with legacy stage values. The classifier transparently maps them so existing data keeps working:
LegacyCurrent
qualificationeligibility
applicabilityfit
suitabilitymatch
To physically migrate the rows in the database (recommended for tidiness), run:
cd platform && npx tsx ../tools/scripts/migrate-decisioning-stage.ts --tenant <tenantId> --apply
The script defaults to dry-run; pass --apply to mutate. It returns a JSON summary of how many rows were updated per legacy value.

Effective Rules — inheritance view per offer

Each offer rolls up rules from four scope levels:
global → category → subcategory → offer
A qualification rule (or contact policy) attached to a category applies to every offer in that category; a rule attached to a subcategory narrows that further; a rule attached to an offer applies only to that offer; a rule with scope = "global" applies to every offer in the tenant. Channel and creative scopes are intentionally excluded from this view. They evaluate at decision time and require a specific channel/creative the offer is being delivered through. Operators inspect those via Decision Traces.

Where to find it

Open any offer in /studio/actions, click into the detail view, and click Effective Rules in the top action bar. The page renders two tables — Contact Policies and Decisioning Rules — each annotated with the matched scope (global / category / subcategory / offer).

API

GET /api/v1/offers/:id/effective-rules
Returns:
{
  "offerId": "<uuid>",
  "categoryId": "<uuid>|null",
  "subCategoryId": "<uuid>|null",
  "contactPolicies": [
    {
      "id": "<uuid>",
      "name": "Daily frequency cap",
      "ruleType": "frequency_cap",
      "config": { "maxPerDay": 3 },
      "priority": 80,
      "status": "active",
      "source": "contact_policy",
      "stage": null,
      "matchedScope": { "scope": "category", "scopeId": "<uuid>" }
    }
  ],
  "qualificationRules": [
    {
      "id": "<uuid>",
      "name": "US residents only",
      "ruleType": "segment_required",
      "config": { "segmentId": "<uuid>" },
      "priority": 100,
      "status": "active",
      "source": "qualification_rule",
      "stage": "eligibility",
      "matchedScope": { "scope": "global", "scopeId": null }
    }
  ]
}
The endpoint requires any of the admin, editor, or viewer roles. Both lists are sorted by priority descending. The matchedScope field tells the UI why the rule applies (e.g., “rule X applies because of category Y”); legacy single-scope rows that haven’t been migrated to the multi-scope scopes[] relation still resolve correctly.