Skip to main content

Overview

Contact Policies are post-qualification rules that remove Offers a customer is eligible for but should not receive right now. The decision-flow engine evaluates them after qualification rules and consent checks, but before scoring and ranking. Each policy inspects materialized interaction summaries (impression counts, last-contact timestamps, outcome history) and either blocks or allows a candidate. Policies are evaluated in priority order (highest first). An allow_override policy that matches a candidate causes the engine to skip all blocking rules for that candidate. Otherwise, the first blocking rule that fires removes the candidate from the result set. Mandatory Offers (marked isMandatory) still respect frequency_cap and cross_channel_cap rules plus any policy where config.bypassable is false. All other blocking rules are skipped for mandatory Offers.

Rule Types

KaireonAI supports 10 rule types. Nine are defined across the validation schema and domain rules; one (metric_condition) lives in the engine and domain layer but is not yet exposed in the API validation enum.
Rule TypeSummaryScopes
frequency_capMax impressions per day / week / month / totalglobal, offer, creative, channel
cooldownMinimum hours between contactsglobal, offer, creative, channel
budget_exhaustedSuppress when impression or spend budget consumedoffer, creative
outcome_basedSuppress after a specific outcome (e.g. complaint)offer, creative
segment_exclusionExclude customers in named segmentsglobal
time_windowRestrict to specific hours and days of the weekglobal, channel
mutual_exclusionIf one Offer in a group was served, suppress the othersoffer
cross_channel_capFrequency cap aggregated across all channelsglobal, offer, creative, channel
allow_overrideExplicitly allow contact despite other blocking rulesglobal, offer, creative, channel
category_suppressionSuppress all Offers in a category after any was shown recentlyglobal
metric_conditionBlock when a behavioral metric crosses a thresholdglobal, offer, creative, channel
metric_condition is implemented in the contact-policy engine but is not included in the API validation enum. All other ten types (including category_suppression) are fully wired end-to-end.

frequency_cap

Limits how many times a customer can be contacted within rolling time windows. You can set one or more caps on the same policy. Config fields:
FieldTypeDescription
maxPerDaynumberMax impressions per calendar day
maxPerWeeknumberMax impressions per ISO week
maxPerMonthnumberMax impressions per calendar month
maxTotalnumberLifetime impression cap
Runtime: The engine aggregates interaction summaries for the matching period and blocks the candidate when impressions >= max*.
{
  "ruleType": "frequency_cap",
  "scope": "channel",
  "scopeId": "ch_email",
  "config": {
    "maxPerDay": 1,
    "maxPerWeek": 3
  },
  "priority": 80
}

cooldown

Enforces a minimum wait period (in hours) since the last contact before the same Offer can be served again. Config fields:
FieldTypeDescription
cooldownHoursnumberMinimum hours between contacts
Runtime: Looks up lastContactAt from interaction summaries and blocks if fewer than cooldownHours have elapsed.
{
  "ruleType": "cooldown",
  "scope": "offer",
  "scopeId": "off_welcome_bonus",
  "config": {
    "cooldownHours": 48
  },
  "priority": 60
}

budget_exhausted

Suppresses an Offer when its impression count or spend crosses a threshold. The engine checks alltime summary records. Config fields:
FieldTypeDescription
budgetField"impressions" | "spend"Which metric to check
thresholdnumberValue at which the Offer is suppressed
Runtime: Reads the alltime summary for the exact offer + creative + channel combination and blocks when the budget field meets or exceeds threshold.
{
  "ruleType": "budget_exhausted",
  "scope": "offer",
  "scopeId": "off_spring_promo",
  "config": {
    "budgetField": "impressions",
    "threshold": 50000
  },
  "priority": 90
}

outcome_based

Suppresses an Offer for a specified number of days after a customer records a particular outcome (e.g. complaint, opt_out). Config fields:
FieldTypeDescription
afterOutcomestringThe outcome key that triggers suppression
suppressForDaysnumberNumber of days to suppress after that outcome
Runtime: Checks the last recorded outcome key and, if it matches afterOutcome, blocks the candidate until suppressForDays have passed.
{
  "ruleType": "outcome_based",
  "scope": "offer",
  "scopeId": "off_credit_card",
  "config": {
    "afterOutcome": "complaint",
    "suppressForDays": 90
  },
  "priority": 85
}

segment_exclusion

Blocks all Offers for customers belonging to one or more excluded segments. This is a global-only rule. Config fields:
FieldTypeDescription
excludeSegmentsstring[]Segment keys to exclude
Runtime: Compares segments from the Recommend request against excludeSegments. If the customer matches any, the candidate is blocked. If no segment data is available in the request, the engine fails open (allows the candidate through) to avoid blocking all offers when segment data is unavailable.
{
  "ruleType": "segment_exclusion",
  "scope": "global",
  "config": {
    "excludeSegments": ["do_not_contact", "legal_hold", "recently_churned"]
  },
  "priority": 100
}

time_window

Restricts contacts to specific hours and/or days of the week. Supports IANA timezone strings validated at write time. Config fields:
FieldTypeDescription
startHournumber (0-23)Start of allowed window (inclusive)
endHournumber (0-23)End of allowed window (exclusive)
daysOfWeekstring[]Allowed days: Mon, Tue, Wed, Thu, Fri, Sat, Sun
timezonestringIANA timezone (e.g. America/New_York). Falls back to UTC if omitted or invalid
Runtime: Converts the current time to the configured timezone, then checks both day-of-week and hour range. Overnight windows (e.g. startHour: 22, endHour: 6) are handled correctly.
{
  "ruleType": "time_window",
  "scope": "channel",
  "scopeId": "ch_sms",
  "config": {
    "daysOfWeek": ["Mon", "Tue", "Wed", "Thu", "Fri"],
    "startHour": 9,
    "endHour": 18,
    "timezone": "America/New_York"
  },
  "priority": 70
}

mutual_exclusion

Prevents competing Offers from being served to the same customer within a time window. If any Offer in the group has been shown recently, the others are suppressed. Config fields:
FieldTypeDescription
offerGroupstring[]List of Offer IDs that are mutually exclusive
suppressForDaysnumberDays to suppress other group members after one is shown (default: 90)
Runtime: For each candidate Offer in the group, checks whether any other Offer in the group has an alltime summary with impressions > 0 and lastContactAt within suppressForDays.
{
  "ruleType": "mutual_exclusion",
  "scope": "offer",
  "scopeId": "off_platinum_card",
  "config": {
    "offerGroup": ["off_platinum_card", "off_gold_card", "off_silver_card"],
    "suppressForDays": 30
  },
  "priority": 75
}

category_suppression

Suppresses all Offers in a category (or sub-category) for a specified number of days after any Offer in that category was shown to the customer. This prevents fatigue from repeated pitches in the same product area. Config fields:
FieldTypeDescription
categoryIdstringThe Category ID to suppress
subCategoryIdstring (optional)Narrow to a specific sub-category
suppressionDaysnumberDays to suppress after any category Offer was shown (default: 7)
Runtime: Builds a map of all Offer IDs in the target category from the current candidate set. Checks alltime summaries for any of those Offers. If any was shown within suppressionDays, all candidates in that category are blocked.
{
  "ruleType": "category_suppression",
  "scope": "global",
  "config": {
    "categoryId": "cat_auto_insurance",
    "suppressionDays": 7
  },
  "priority": 80
}

cross_channel_cap

Like frequency_cap, but aggregates impressions across all channels for the same Offer within a period. Config fields:
FieldTypeDescription
periodType"daily" | "weekly" | "monthly"Aggregation period (default: daily)
maxTotalnumberMax total impressions across all channels in the period
Runtime: Sums impressions across all channel summaries for the Offer in the current period and blocks when the total meets or exceeds maxTotal.
{
  "ruleType": "cross_channel_cap",
  "scope": "global",
  "config": {
    "periodType": "weekly",
    "maxTotal": 5
  },
  "priority": 80
}

allow_override

An override that allows contact despite other blocking policies. When the engine finds a matching allow_override, it skips all blocking rules for that candidate. Use this for mandatory or time-sensitive Offers that must bypass normal frequency limits. Config fields:
FieldTypeDescription
allowSegmentsstring[](Optional) Only apply override if customer is in one of these segments
allowOfferIdsstring[](Optional) Only apply override for these specific Offer IDs
Runtime: allow_override policies are evaluated before all blocking rules. If both allowSegments and allowOfferIds are set, both conditions must match. The engine logs a warning for every override to maintain an audit trail.
Use allow_override sparingly. Every override is logged with a warning. Consider making overrides time-bounded by pausing or archiving them after the campaign ends.
{
  "ruleType": "allow_override",
  "scope": "offer",
  "scopeId": "off_regulatory_notice",
  "config": {
    "allowOfferIds": ["off_regulatory_notice"]
  },
  "priority": 100
}

metric_condition

Blocks candidates when a behavioral metric value crosses a threshold. Supports dimension mapping to resolve metric values per candidate.
This rule type is implemented in the contact-policy engine and domain layer (contact-policy-rules.ts) but is not yet included in the API validation schema (api-validate.ts). To use it today, add it to the ruleType enum in the validation schema or write policies directly to the database.
Config fields:
FieldTypeDescription
metricIdstringThe behavioral metric to evaluate
operator"gt" | "gte" | "lt" | "lte" | "eq"Comparison operator (default: gte)
thresholdnumberValue that triggers suppression
dimensionMappingRecord<string, string>Maps metric dimensions to candidate fields (e.g. {"channel": "$candidate.channelId"})
Runtime: Looks up the metric value using the dimension mapping, applies the operator, and blocks if the condition is met.
{
  "ruleType": "metric_condition",
  "scope": "global",
  "config": {
    "metricId": "met_7d_impression_count",
    "operator": "gte",
    "threshold": 10,
    "dimensionMapping": {
      "channel": "$candidate.channelId"
    }
  },
  "priority": 70
}

Volume Constraints

Volume Constraints are system-wide caps that limit the total number of times an offer, category, or channel can be recommended across all customers within a time period. Unlike frequency caps (which are per-customer), volume constraints enforce business-level limits such as “no more than 20,000 email impressions per week” or “limit the Gold Card offer to 5,000 recommendations per month.”

How They Differ from Contact Policies

Contact PoliciesVolume Constraints
ScopePer-customerSystem-wide (all customers)
Question answered”Has this customer been contacted too often?""Has this offer/channel been used too much overall?”
Evaluation pointAfter qualification, before scoringAfter scoring, before response assembly
Typical useFrequency caps, cooldownsInventory caps, channel throughput limits, campaign budgets

Configuration

Each volume constraint specifies a scope, period, and maximum count:
FieldTypeRequiredDescription
namestringYesHuman-readable name
scope"offer" | "category" | "channel"YesWhat entity the cap applies to
scopeIdstringYesThe ID of the scoped entity
period"daily" | "weekly" | "monthly"YesRolling time window
maxCountintegerYesMaximum recommendations allowed in the period
status"active" | "paused"NoDefault "active"
Example: Limit the “Premium Card” offer to 10,000 recommendations per week:
{
  "name": "Premium Card Weekly Cap",
  "scope": "offer",
  "scopeId": "offer_premium_card",
  "period": "weekly",
  "maxCount": 10000,
  "status": "active"
}

Counter Mechanism

Volume counters are auto-incremented when impression outcomes are recorded via the Respond API. This means:
  • The counter reflects actual deliveries, not just recommendations
  • A recommendation that is never delivered does not consume volume
  • Counters reset automatically at the start of each period (midnight UTC for daily, Monday 00:00 UTC for weekly, first of month for monthly)

API

MethodEndpointDescription
GET/api/v1/volume-constraintsList all volume constraints
POST/api/v1/volume-constraintsCreate a new volume constraint
PUT/api/v1/volume-constraintsUpdate an existing volume constraint (requires id)
DELETE/api/v1/volume-constraints?id={id}Delete a volume constraint
Use volume constraints alongside contact policies for complete control. Contact policies protect individual customers from over-contact; volume constraints protect your business from over-committing inventory or exceeding channel capacity.

Scopes

Every Contact Policy operates within one of four scopes, which determines which candidates it applies to.
ScopeMatches WhenTypical Use
globalAlways matches every candidateCompany-wide compliance rules
offerscopeId equals the candidate’s Offer IDProduct-specific frequency limits
creativescopeId equals the candidate’s Creative IDCreative-level fatigue rules
channelscopeId equals the candidate’s Channel IDChannel-specific time windows or caps
Not every rule type supports every scope. The table in the Rule Types section above shows allowed scopes per rule type. The UI dynamically filters the scope dropdown based on your selected rule type.
Use global scope for company-wide compliance rules (e.g. “no more than 5 contacts per week across all channels”) and narrower scopes for product-specific constraints.

Priority and Conflict Resolution

Each Contact Policy has a priority value from 0 to 100 (default: 50). Higher values are evaluated first. The engine resolves conflicts as follows:
  1. Sort all active policies by priority descending.
  2. Separate allow_override policies from blocking policies.
  3. For each candidate, check allow_override policies first. If any matches, the candidate is explicitly allowed and all blocking rules are skipped.
  4. Otherwise, evaluate blocking policies in priority order. The first rule that blocks removes the candidate.
  5. Unknown rule types fail open (the candidate is allowed through) with a warning log. This prevents misconfigured rule types from silently killing all offers, but also means invalid policies provide zero protection.
This means a priority-100 allow_override will beat a priority-100 frequency_cap because overrides are always checked first regardless of priority.

Suppressions (Pre-Computed Enforcement)

Starting with recent releases, contact policies are enforced via pre-computed suppression records rather than being evaluated live at decision time. This architecture dramatically reduces latency in the Recommend API by replacing per-policy evaluation with a single database read.

How It Works

  1. Write path (Respond API): When a respond call records an outcome that triggers a policy threshold (e.g., a frequency cap is reached, a cooldown begins), the engine writes a suppression record to the database with an expiry timestamp and the relevant scope (offer, creative, channel, or category).
  2. Read path (Recommend API): At decision time, the engine loads all active (non-expired) suppressions for the customer in one query. Any candidate matching a suppression is immediately removed — no per-policy evaluation needed.
This means the cost of contact policy enforcement at decision time is constant regardless of how many policies are configured.

Pre-Computed vs Live-Evaluated Policy Types

Not all policy types can be pre-computed. Policies that depend on the current request context (time of day, customer segments in the request payload) must still evaluate live.
EnforcementPolicy Types
Pre-computed (suppression records)cooldown, outcome_based, mutual_exclusion, category_suppression, frequency_cap, cross_channel_cap, budget_exhausted
Live-evaluated (checked at decision time)time_window, segment_exclusion, allow_override
The seven pre-computed types cover the vast majority of contact policies. The three live-evaluated types are inherently request-dependent and cannot be pre-computed.

Suppression Expiry Modes

Each suppression record carries an expiresAt timestamp. The engine supports three expiry calculation modes:
ModeBehaviorExample
exactExpires exactly N hours from nowA 48-hour cooldown expires at the same time of day, two days later
calendar_dayExpires at midnight (UTC) on day NA 7-day suppression created on Monday expires at midnight the following Monday
end_of_dayExpires at midnight (UTC) tonightA daily frequency cap resets at the start of the next calendar day

Audit Trail

Every suppression record embeds evidence — a JSON object containing the policy ID, rule type, the threshold that was crossed, and the interaction that triggered it. This evidence is preserved for the lifetime of the suppression and is surfaced in decision traces when debug mode is enabled.
{
  "customerId": "C-4821",
  "scope": "channel",
  "scopeId": "ch_email",
  "reason": "frequency_cap",
  "policyId": "cp_email_weekly",
  "expiresAt": "2026-03-30T00:00:00Z",
  "expiryMode": "end_of_day",
  "evidence": {
    "ruleType": "frequency_cap",
    "threshold": { "maxPerWeek": 3 },
    "actual": 3,
    "triggeredBy": "interaction_abc123"
  }
}

Escalating Suppressions

When a customer repeatedly triggers the same policy, the suppression duration escalates automatically. Each suppression record tracks a triggerCount that increments on each re-trigger, and the engine looks up the matching escalation tier to determine how long the next suppression lasts. Configuration: Add an escalation array to any suppression-eligible policy’s config alongside the base cooldownHours:
{
  "cooldownHours": 48,
  "escalation": [
    { "trigger": 2, "durationDays": 14 },
    { "trigger": 3, "durationDays": 90 },
    { "trigger": 4, "action": "permanent" }
  ]
}
How it works:
Trigger CountBehavior
1 (first trigger)Base suppression duration applies (e.g. cooldownHours: 48)
2Matches trigger: 2 tier — suppressed for 14 days
3Matches trigger: 3 tier — suppressed for 90 days
4+Matches trigger: 4 with "action": "permanent" — suppressed for 10 years
Reset behavior: If the customer does not re-trigger the policy within 7 days of the current suppression’s expiry, the triggerCount resets to zero. The next violation starts fresh at tier 1.
Use escalating suppressions for policies like outcome_based (e.g. complaints) or cooldown where repeat offenders should face progressively longer quiet periods. The "permanent" action is implemented as a 10-year suppression to avoid infinite timestamps.

Field Reference

All fields accepted by the POST /api/v1/contact-policies endpoint:
FieldTypeRequiredDefaultDescription
namestringYesUnique policy name
descriptionstringNo""Human-readable description
status"draft" | "active" | "paused" | "archived"No"active"Only active policies are evaluated at decision time
scope"global" | "offer" | "creative" | "channel"No"offer"Which entity level this policy targets
scopeIdstring | nullNonullThe ID of the scoped entity (required unless scope is global)
ruleTypeenum (see Rule Types)YesOne of the nine API-supported rule types
configobjectNo{}Rule-specific configuration (see each rule type above)
priorityinteger (0-100)No50Evaluation order; higher = evaluated first

Worked Example

Setup

Customer C-4821 has already received 3 emails this week for the “Spring Promo” Offer. A frequency_cap policy limits the email channel to 3 per week. Policy:
{
  "id": "cp_email_weekly",
  "name": "Weekly Email Cap",
  "ruleType": "frequency_cap",
  "scope": "channel",
  "scopeId": "ch_email",
  "config": { "maxPerWeek": 3 },
  "priority": 80,
  "status": "active"
}

Step 1 — Frequency Cap Blocks

When the Recommend API runs for customer C-4821, the engine:
  1. Loads the weekly interaction summary: impressions = 3 for ch_email in the current ISO week.
  2. Evaluates frequency_cap: 3 >= 3 (maxPerWeek) — BLOCKED.
  3. The candidate is removed from the result set.
Decision trace (debug mode):
{
  "contactPolicyReasons": [
    {
      "offerId": "off_spring_promo",
      "creativeId": "cr_spring_email_v2",
      "reason": "Weekly frequency cap reached: 3/3",
      "policyId": "cp_email_weekly"
    }
  ]
}

Step 2 — Allow Override Bypasses the Cap

Now suppose a regulatory notice must reach C-4821 regardless of frequency limits. An allow_override policy exists:
{
  "id": "cp_regulatory_override",
  "name": "Regulatory Notice Override",
  "ruleType": "allow_override",
  "scope": "offer",
  "scopeId": "off_regulatory_notice",
  "config": {
    "allowOfferIds": ["off_regulatory_notice"]
  },
  "priority": 100,
  "status": "active"
}
The engine evaluates allow_override policies before blocking rules. Because this override matches the regulatory notice Offer, the frequency_cap is never checked for that candidate. The regulatory notice is delivered. The engine logs a warning:
WARN [contact-policy-engine] allow_override policy bypassing contact policy
  — ensure this override is time-bounded and approved
  { policyId: "cp_regulatory_override", offerId: "off_regulatory_notice" }

API Quick Reference

MethodEndpointAuthDescription
GET/api/v1/contact-policiesviewer, editor, adminList policies (paginated, sorted by priority desc)
POST/api/v1/contact-policiesadminCreate a new policy
PUT/api/v1/contact-policiesadminUpdate an existing policy (requires id in body)
DELETE/api/v1/contact-policies?id={id}adminSoft-delete a policy
Deleting a policy uses soft-delete (the record is retained with a deletedAt timestamp). If the policy is referenced by any Decision Flow’s draftConfig, the response includes a warnings array listing affected flows (ghost reference check). Updates use auditedUpdate to create audit snapshots and increment rowVersion. See the API Reference for full request and response schemas.

Contact Policies vs Volume Constraints

Contact policies and volume constraints are complementary mechanisms that are both enforced during the Contact Policy pipeline stage, but they protect different things:
Contact PoliciesVolume Constraints
ProtectsIndividual customersThe business
PurposeFrequency caps, cooldowns, suppressionsBudget caps, inventory limits, channel quotas
ScopePer-customer interaction historySystem-wide counters across all customers
Evaluation order: Contact policies are evaluated first, then volume constraints filter the remaining candidates. A customer may pass all contact policy checks but still be blocked by a volume constraint if the offer, category, or channel has reached its delivery cap.
Use both together for complete control. Contact policies prevent individual customer fatigue; volume constraints prevent over-committing inventory or exceeding channel capacity. See the Volume Constraints page for configuration details.

Qualification Rules

Rules evaluated before Contact Policies that determine initial Offer eligibility.

Volume Constraints

System-wide delivery caps on offers, categories, and channels.

Behavioral Metrics

Create metrics from interaction data to drive metric_condition policies.

Decision Flows

The pipeline that orchestrates qualification, contact policies, scoring, and ranking.