Skip to main content
This is the platform changelog. It tracks notable user-facing changes to KaireonAI — new features, APIs, UI additions, and behavioral shifts — in reverse-chronological order. New capabilities land here when they are deployed to playground.kaireonai.com. For the forward-looking view, see the roadmap.

2026-07-03 — Agentic AI: governed assistant, Recommendation Inbox, Autopilot, Sentinel, cold-start priors, hosted MCP

Enforced preview→approve for every AI assistant write. All entity-writing chat tools — including creates, model training, and predictor changes, which previously executed the moment the model called them — now return a preview card and execute only after explicit approval via the new POST /api/v1/ai/mutations/confirm (audit-logged, no LLM round-trip). See AI Assistant. Recommendation Inbox on the AI sidebar. The Segments, Policy Recommendations, and Content Intelligence pages now run their namesake LLM analyzers on demand and persist actionable findings as reviewable recommendations (deduplicated); AI Insights hosts the unified inbox. Applying creates draft entities — or performs scoped changes for the new experiment / model / weights types. A failed apply keeps the recommendation open. See AI Insights. Decisioning Autopilot. A 6-hour background sweep turns experiment champion/challenger results and significant model drift into inbox proposals, governed by a per-tenant autonomy mode: suggest (default), auto_gated (opens four-eyes Approval Requests), or auto (applies immediately, audit-logged). Configure in Settings > AI Configuration > AI Autonomy. See Decisioning Autopilot. Decision Sentinel. Two new alert metrics — suppression_rate and empty_candidate_rate — watch the decision funnel every 30 minutes; breaches raise System Health alerts, and hard breaches can auto-pause active flows for tenants that opt in. Both metrics are also available to custom alert rules and seeded as defaults for new tenants. See Decision Sentinel. Cold-start priors. New offers with a category seed informed propensity priors from same-category offers (10 pseudo-observations at the evidence-weighted neighbor mean) instead of a flat prior; real outcomes wash them out quickly and existing evidence is never overwritten. See Adaptive Learning. Hosted MCP endpoint. POST /api/v1/mcp exposes the full MCP tool surface (172 tools + playbooks) over stateless JSON-RPC with API-key auth, per-call audit logging, tenant pinning, and governed playbook applies (mutations queue for approval instead of writing directly). See MCP server.

2026-07-03 — Decisioning correctness fixes + offer_attribute qualification rule; real logistic-regression training; hardened API validation

New qualification rule type: offer_attribute. Filters on offer-level fields (productType, margin, revenueValue, or custom fields) rather than customer attributes — the offer-side counterpart to attribute_condition. Same operator set, except offer_attribute supports not_in where attribute_condition supports contains. See Qualification Rules and the API reference. allow_override contact policy fix. An override configured with only allowMandatory: true previously un-blocked every candidate, not just mandatory Offers — a governance hole. allowMandatory alone now un-blocks mandatory Offers only; use allowSegments/allowOfferIds to gate by segment/offer, or an empty {} config for an intentional blanket override. See Contact Policies. logistic_regression now trains real weights. The model fits genuine coefficients via full-batch gradient descent, consuming learningRate (0.05), maxIterations (200), and regularization (L2 λ, 0) from model.config — these were previously inert. A real fit needs ≥ 20 usable labeled rows spanning both classes; below that, training falls back to a metrics-only pass rather than producing a degenerate model. See Logistic Regression. Hardened API validation.
  • POST /api/v1/outcome-types returns 409 (was 500) when the key already exists for the tenant.
  • Offers and creatives now validate foreign-key tenant ownership. A categoryId/subCategoryId (offers, sub-categories) or offerId/channelId/placementId (creatives) that doesn’t belong to your tenant is rejected with 400 instead of being silently linked.
  • Mandatory-override guards now apply to updates. Turning an Offer mandatory via PUT /api/v1/offers runs the same governance validation as create (future expiry, real in-tenant admin approver) — the update path can no longer bypass it.
  • GET /api/v1/cron/scheduled-retrains returns 401 (was 503) when CRON_SECRET is unset, consistent with the rest of the cron tier’s fail-closed auth.

2026-06-08 — Cross-offer constraints CRUD API; realtime ranking flags configurable via analyzer-settings API and UI; analyzer-settings persistence fix

Cross-offer constraints CRUD API is now public. GET/POST/PUT/DELETE /api/v1/cross-offer-constraints lets operators create and manage cross-offer ranking constraints consumed by the realtime Lagrangian solver on /recommend. Three rule types are available: channel_quota (cap offers per channel), portfolio_budget (cap total spend across a set of offers), and category_cap (cap offers per category). Constraints are activated by enabling tenantSettings.aiAnalyzerSettings.ranking.lagrangianEnabled. See Cross-Offer Constraints API. Realtime ranking flags are now configurable via PUT /api/v1/ai/analyzer-settings. A new ranking object (validated by RankingSettingsSchema) exposes five boolean opt-in flags, all defaulting to false:
  • lagrangianEnabled — gates the realtime Lagrangian shadow-price solver on the /recommend hot path. When on and any offer has a binding budget.dailyCapCents, the solver applies a continuous penalty that rotates traffic toward less-saturated offers without hard-dropping them. Cross-offer constraints are also loaded on this path when active rows exist. Verified against src/lib/ranking/apply-lagrangian.ts + realtime-wire.ts.
  • crossOfferEnabled — gates cross-offer constraint loading on the batch path (batch-executor.ts). Requires lagrangianEnabled: true. Note: on the realtime /recommend path, cross-offer constraints are loaded whenever lagrangianEnabled is true — crossOfferEnabled is a batch-path-only gate. See Cross-Offer Constraints API for the full runtime-path breakdown.
  • exp3IxEnabled — gates the EXP3-IX online bandit. A no-op unless banditArms are operator-configured; see EXP3-IX Ranking.
  • budgetPacingEnabled and goalSeekEnabled — gating toggles for budget-pacing and goal-seek wires; full operator configuration UI is forthcoming. Enabling without operator config is a safe no-op.
The flags are read by GET /api/v1/ai/analyzer-settings (viewer+) and written by PUT /api/v1/ai/analyzer-settings (admin). The UI Settings > AI Configuration page surfaces them in the new Ranking card. Analyzer-settings persistence fix. PUT /api/v1/ai/analyzer-settings now reads and writes the TenantSettings table — the same JSON column the /recommend runtime reads at decision time. Previously the route wrote to a table that the runtime did not consult, so saved settings had no effect on live decisions. Settings now take effect on the next /recommend call with no server restart required. Existing sibling keys in the aiAnalyzerSettings blob (e.g. llmExplanationsEnabled) are preserved across writes.

2026-06-07 — Security hardenings, key rotation, DSAR download, respond attribution fix, dashboard panels

Five security hardenings shipped together.
  • Timing-safe cron/trigger secret comparison — every CRON_SECRET / shared-secret check across the cron and trigger routes now uses a constant-time comparison, eliminating a timing side-channel. The final three routes were migrated to close the gap: cron/approvals-expire, cron/outbox-reaper, and triggers/file-arrival.
  • Logger auto-redaction — the platform logger automatically redacts sensitive keys (tokens, secrets, passwords, API keys, etc.) from structured log payloads so they cannot appear in log sinks.
  • MFA step-up proof is now server-issued. A successful TOTP or WebAuthn verify mints an HMAC-SHA256-signed kaireon_stepup httpOnly cookie; middleware validates the cookie. The old session.update({ mfaVerifiedAt }) path that let a client mint its own freshness is no longer trusted. See MFA enforcement.
  • API key scopes for SCIM. ApiKey.scopes is now a JSON column (default []). Non-empty scopes restrict a key to listed endpoints; SCIM endpoints require the "scim" scope. See API Keys and SCIM v2.
  • Tenant-scoped profile lookup — the customer profile route no longer returns cross-tenant data on an edge case.
Encryption key rotation route. New POST /api/v1/encryption/rotate (admin; MFA step-up for session admins, key-possession for API-key callers) re-encrypts four stores — connectors, platform settings, SSO OIDC secrets, MFA TOTP secrets — in a single call. Supports dryRun: true for pre-flight inspection. Row cap of 5,000 per store per pass; re-run for larger tenants. See Encryption Key Rotation. DSAR deliverable export. A new dsar_exports table (migration 28) persists export payloads at completion time. GET /api/v1/dsar/{id}/download (admin) returns the payload as a JSON file attachment. Encrypted exports are delivered as-is with X-Kaireon-Payload-Encrypted: true. Payloads age out on the “decisions” retention class; DsarRequest rows are never purged. Exports completed before 2026-06-07 are not downloadable — re-run the export. See DSAR. Respond attribution fix. /respond now honors recommendationId end-to-end. /recommend stamps the recommendationId column on recommendation and auto-impression rows. /respond uses a three-tier lookup ladder — (1) column match, (2) legacy JSON match for pre-fix rows, (3) rank-only fallback — so attribution remains precise for old rows. Limitation: multi-placement /recommend responses write no interaction rows, so precise attribution applies to single-flow recommendations and auto-impressions. See Respond API. Dashboard improvements.
  • Operations — Decision Pipeline panel now reads persisted DecisionTrace aggregates via GET /api/v1/dashboards/decision-pipeline (window 1–168 hours, default 24). The panel header shows the effective window from the API response. Filter rates are labeled as cumulative. The period selector correctly refetches the panel on change (was stuck at 7d). Empty state mentions decisionTraceSampleRate.
  • Model Health — four new Analysis panels: Uplift (per-creative observational uplift with 95% CI, 7/30/90d window), CATE Explorer (per-customer τ with segment badges, model-picker + method select), Fairness (inline-mode paste-and-evaluate — no persisted history), Drift Check (paste reference/current distributions → PSI/KS per feature).
  • Attribution model picker shows two disabled Coming Soon entries: Shapley and Cross-Device. The API enum is unchanged — five live models remain selectable.

2026-06-06 — Guardrail enforcement, four-eyes publish gate, CLV/uplift PRIE weights

Guardrails are now enforced on every Recommend request. Active guardrail rules run during the decision pipeline — once per request at rank-node entry (so ranking and the result limit operate on the survivors, and expressions can reference offer.score), with a response-node fallback for flows without a rank node. A rule’s expressionAst describes when it fires: a firing hard rule blocks the candidate (audit-logged as mandatory_override), a firing soft rule keeps it but logs a warning. A malformed or unrecognized expression never fires — guardrails fail open at the candidate level, so a mistyped rule degrades to a no-op rather than suppressing every offer. Enforcement is independent of contact policies: skipContactPolicy does not skip guardrails, and it is not gated by the enableGuardrails module flag. Rules load fail-open (a load failure leaves the request unfiltered with a loud log), are cached 300 s, and the cache is invalidated on every guardrail create/update/delete. debugTrace.afterGuardrails is now the real post-guardrail count, and debugTrace.guardrailReasons[] lists each failed evaluation ({ ruleKey, ruleName, severity, passed, reason?, offerId? }). Four-eyes publish gate (opt-in). The new tenant setting requirePublishApproval (default false, toggle on the Settings page) gates decision-flow publish: when on, publish requires a fresh approved ApprovalRequest (entityType=decisionFlow, action=publish, matching entityId). One approval authorizes exactly one publish — the publish stamps the approval id onto the new version, so reuse is rejected. A blocked publish returns 422 (reason: "no_approval" | "consumed") and writes a publish_blocked audit entry. The gate fails closed if the settings lookup errors. Because stage walking already rejects self-approval and duplicate approvers, an approved request implies two distinct identities. Migration: prisma/manual-sql/26_tenant_require_publish_approval.sql. CLV and uplift weights in PRIE ranking (default 0 = no change). The formula Score node gains two optional exponent terms outside the P+R+I+E sum-to-1 constraint: upliftWeight and clvWeight (each 0..2 inline, 0..1 via a ranking profile). The ranking-profile weight keys uplift and clv now map straight into the formula — previously upliftWeight was documented but stripped by validation, so it is now actually reachable. The CLV term applies score ×= impact^(clvWeight × clvNorm), where clvNorm = clvScore/100 from the customer’s CLV row, so high-CLV customers get up to clvWeight extra impact emphasis (no CLV row → term skipped; lookup cached 300 s). The linear multi-objective scorer (computeArbitratedScore) also gains a sixth clv objective (default 0). Trace fields clvNorm and clvImpactExponent are stamped per candidate when the term is active. Studio surfaces Uplift and CLV sliders under Scoring Strategies.

2026-05-13 — Scoring fixes (#202 / #204 / #212), excludeJoinIds, implicit Contact Policy

model_adaptations global-scope upsert (#202). PostgreSQL treats NULL != NULL in unique constraints, so the engine’s ON CONFLICT (tenantId, modelId, scope, scopeId) never fired on global rows where scopeId was NULL — every Respond accumulated a duplicate row instead of incrementing positives/negatives/evidence. Fixed by using "" as the global scope sentinel everywhere (respond, train, reset). Read path tolerates both "" and legacy NULL during rollout via a.scopeId ?? "". Verified live: 5 responds × 9 active models now produce exactly 9 single global rows with evidence=5 each (not 45 duplicate rows with evidence=1). Bandits + neural CF wiring to per-candidate scoring (#204). In the propensity scoring path, scoreWithModel expected attributes.offerIds (plural) for Thompson and ε-greedy and attributes.{customerId, offerId} for neural CF — but the engine passed neither. All three model types short-circuited to a constant 0.5 for every candidate regardless of model state. Fixed by injecting offerId, offerIds: [offerId], and customerId into the per-candidate scoringAttributes before invoking scoreWithModel. Bandits now sample per-arm; neural CF runs the embedding lookup; all three respect their learned state. Maturity-ramp cold-start floor is tenant-configurable (#212). The implicit 0.20 floor inside applyMaturityRamp made per-customer A/B testing unreliable: two customers comparing the same offer each rolled their own deterministic-random seed, so there was a ~64% chance that at least one customer saw the offer dropped. Default raised to 0.50 (collision risk drops to 25%); new maturityRampColdStartFloor tenant setting lets operators tighten back to 0.20 (legacy) or 0.0 (strictest ramp), or disable the ramp entirely with modelMaturityThreshold = 0. Per-flow auto-enrich opt-out (#168). EnrichNodeConfig.excludeJoinIds[] lets a single flow skip selected schema joins from autoEnrich=true without disabling the join globally. The UI shows auto-enriched join chips with an × button on the Enrich panel. Contact Policy is now implicit (#169). Every flow runs the active contact policy stage unless DecisionFlow.skipContactPolicy = true. Operators no longer have to remember to drag a Policy node into every flow — the engine injects one automatically. The opt-out exists for test flows, transactional channels, and one-off promotional sends. Score / propensity hardening:
  • propensityScoreFloor (default 0.05) prevents starvation — an offer with positiveRate=0 can still earn a small exploration tail through 0.05 × fitMult.
  • applyRankingInfluencers now clamps boosted scores to [0, 1] so propensities above 1.0 never reach the Rank / Group / response payload.
  • no_action outcomes correctly increment evidence (denominator) without touching positives or negatives.
Off-by-one and reliability fixes:
  • offer_category_cap no longer over-counts — InteractionSummary now has an offerCategory denormalized column, and contact-policy evaluation requires an explicit match.
  • DecisionFlow.rankingProfileId and couplingOverride plumb through the PUT API correctly.
  • Draft / paused / archived flows are refused by the engine instead of being silently evaluated.

2026-05-12 (later) — Channel coupling, compute error surface, DNC

Channel-level coupling replaces Group.allowPartial. The legacy all-or-nothing allowPartial: false switch was a coarse fail that didn’t distinguish between “empty due to operator’s choice” and “empty due to contact policy fatigue”. It’s now deprecated and a no-op (the field still parses to keep existing IRs valid). Per-channel coupling takes its place:
  • Channel.couplingMode ("partial" default, "atomic" opt-in) — when atomic, an empty placement in this channel suppresses sibling placements in the same channel only. Cross-channel coupling is intentionally NOT supported — different channels are different attention surfaces.
  • DecisionFlow.couplingOverride — per-flow override that beats the channel default. Useful when one channel serves both atomic flows (e.g. weekly digest email) and partial flows (e.g. transactional email).
  • The post-group coupling pass writes trace.summary.channelCoupling[] so consumers can distinguish “we cascaded because X” from “this placement wasn’t configured.”
Compute formula errors surface in the response. Formula failures (missing customer field, divide-by-zero, type mismatch) used to be silently logged and the candidate’s personalization key just went missing. Now each failure lands in personalization._errors[] on the candidate with { name, kind, formula, error }, plus a count in trace.summary.computeErrors. The candidate stays in the response — operators can filter upstream if they want missing fields to drop the candidate entirely. do_not_contact contact-policy ruleType is now properly wired. It was seeded by industry-accelerator templates but the contact-policy engine had no explicit case — every DNC’d candidate fell through to the fail-closed default branch with a misleading “Unknown rule type” error log. Now there’s an explicit case "do_not_contact" that blocks with a clear reason and policy id. This is the only contact policy that suppresses across channels (legal/regulatory boundary).

2026-05-12 — File-arrival triggers (push + poll) + blue_green column cast

Triggers are now first-class — file_arrival no longer waits inside a scheduled run; the run itself is fired by the sentinel landing.
  • Poll path — the in-process scheduler tick now sweeps every active pipeline with trigger.kind === "file_arrival" alongside its schedule sweep. It probes the source’s configured path (via the same cloud-store wiring the source executor uses), matches keys against the trigger’s per-pipeline controlFilePattern, debounces against lastRunAt + debounceSeconds, and dispatches the run in-process.
  • Push path — new POST /api/v1/triggers/file-arrival endpoint accepts both native {pipelineId, tenantId, objectKey} payloads and S3 EventBridge envelopes. Self-authorizes via CRON_SECRET. Sub-second latency when S3 → EventBridge → API Destination is wired up.
  • Per-pipeline maskscontrolFilePattern lives on the trigger, so multiple pipelines watching the same inbox each declare their own sentinel (customers.done, accounts.done, propositions.done). Drop one → only the matching pipeline fires.
  • Sentinel cleanup — after a successful fire, the sentinel is archived to atomicity.successFolder (or failureFolder on a failed run) so the same trigger doesn’t re-fire on every tick.
  • Deadline enforcementtrigger.deadline.windowMinutes / onMiss is now actually checked. alert emits a system-health warning; fail writes a synthetic failed PipelineRun so SLA dashboards count the miss; skip logs and continues. Dedup key is the pipeline’s lastRunAt so the action fires once per missed window.
Blue_green load mode bug fixrunBlueGreen previously did INSERT INTO <table>_new SELECT * FROM <staging>, which Postgres rejected with 42804 (column "created_at" is of type timestamptz but expression is of type text) because staging columns are TEXT and target columns are typed. The target executor now passes its column-aware projection (NULLIF + ::pgCastType) into runBlueGreen, so blue_green produces the same explicit casts that append/truncate/upsert do. A leftover <table>_new from a prior failed run is dropped before the new CREATE-LIKE. LineageTab key warning — the row map was using <>...</> shorthand which doesn’t accept props; switched to <Fragment key={...}> so React stops warning about missing keys.

2026-05-02 (evening) — Schema-joins UX + customer profile lookup

  • Customer Lookup was returning “no customer found” for valid IDs because the profile route only matched schemas with entityType="customer". The schema-create form sets schemaType="customer" but leaves entityType at its default “custom”, so the lookup missed the actual customer table. Fixed: the route now matches either field.
  • Schema-joins page redesigned around the customer-as-primary best practice. Primary schema is no longer a picker — the tenant’s customer schema is auto-resolved and shown as a read-only chip with its primary key. The foreign-schema dropdown excludes the customer schema (no self-joins). Foreign-key column auto-populates when a column matching the primary key exists, otherwise the user picks from a dropdown of the foreign schema’s actual columns. The redundant primary-schema field picker was removed.

2026-05-02 (late PM) — Three bugs caught in testing

Surfaced during the IAM-role + custom-PK end-to-end test:
  • Schema detail no longer fakes an id BIGSERIAL row when the user defined a custom PK. The list of AUTO columns now matches the actual ds_* table contents — created_at + updated_at only (when a custom PK is set), or those plus id (when no custom PK).
  • Pipeline Runs rowsProcessed was 4× the real count because the run-handler reducer summed rowsLoaded + rowsOut across every node. Now it only sums target rows. Existing run records in the test tenant were backfilled.
  • Sample-row preview evaluates the common single-call formulas inline (concat, coalesce, min, max, round, abs, identity reference). Complex formulas still defer to the runtime but with a friendlier placeholder. Default sample row no longer miscategorises state (and other fields containing “at”) as a date.
Companion to the data-platform end-to-end walkthrough also added today: API Walkthrough.

2026-05-02 (also PM) — {YYYY-MM-DD} token expansion fix

Found during the same E2E test: archived files in S3 / GCS / Azure were landing at literal .archive/{YYYY-MM-DD}/ instead of .archive/2026-05-02/. The local_fs archive helper expands tokens, but the cloud-store impls took the destination verbatim. The source executor now expands {YYYY-MM-DD} / {YYYYMMDD} / {YYYY} / {MM} / {DD} / {HH} / {mm} / {ss} before calling store.archive().

2026-05-02 (PM) — System Health + load-mode safety

Two coupled shipments in the same day: System Health widget (docs). New top-bar Activity icon — distinct from the bell — surfaces operational alerts across the platform. DB-backed with per-user read state. New API at /api/v1/system-health (GET cursor-paginated, PATCH read, POST read-all, DELETE dismiss). Severity taxonomy info | success | warning | error | critical; 30-second polling that pauses on tab background; error/critical route to existing external Slack/Teams/email providers when configured. A retention purge cron at /api/v1/cron/system-health-purge honors each tenant’s retention settings (data class system_health, default 90 days). The previously dead bell icon + in-memory notification store are retired. Load-mode safety (docs). Real data-loss footguns closed:
  • truncate and blue_green now run an empty-source guard by default — pipelines no longer wipe live tables when the upstream produces 0 rows. Override per-target with failOnEmptySource: false.
  • truncate and incremental_watermark wrap the destructive + INSERT statements in prisma.$transaction so a failed INSERT rolls back the TRUNCATE / watermark advance.
  • incremental_watermark now persists the high-water in a pipeline_watermarks checkpoint table (falls back to MAX(target.col) on first run for backwards compat).
  • upsert with all columns in upsertKey now throws at SQL build time instead of silently emitting ON CONFLICT DO NOTHING. UI + parsePipelineIR block the misconfiguration before save.
  • TargetForm gains: full-refresh-shape hard warning recommending blue_green over truncate, failOnEmptySource toggle for destructive modes, mode-switch validation matrix, dedicated config panes for upsertKey / watermarkColumn / cdcSource.
  • Source onMissAction: alert finally fires an actual alert into System Health.
  • Optional expectedRowCountDelta per target node emits a warning alert when today’s load is wildly outside the recent average.
DataSchema docs clarified: marking a schema field as the primary key skips the auto-generated id BIGSERIAL column at table creation; the runtime has supported user-defined PKs since the DDL helper landed. Docs: System Health · Loading Modes · File Ingestion · Data Model

2026-05-02 — Flow editor UX cleanup

A focused pass cleaned up rough edges in the IR-native flow editor:
  • Lineage tab no longer 500s on tables with bigint or Postgres numeric/decimal columns. The lineage payload now serialises large integers as strings (preserves precision past Number.MAX_SAFE_INTEGER) and renders Decimal columns in their human-readable form, so lifetime_value etc. render as "10552.97" instead of internal-state JSON. See Flow Lineage.
  • Pipeline Runs heading aligned with sidebar — the standalone runs page H1 now matches the “Pipeline Runs” sidebar entry, the table uses fixed column widths via shadcn <Table>, and the Error column truncates with a title= tooltip for the full text.
  • Editor is now 2-pane — the redundant “Recent runs” left pane was removed; the bottom strip + dedicated /data/flow-runs page cover run history. Center pane fills the freed width.
  • Branch node formthen and default route inputs are dropdowns of existing IR nodes (excluding self + sources). Stale refs render with a red border.
  • Enrich node form — output-field input now badges columns missing from the destination schema and offers a one-click ”+ Add as <dataType>” button that POSTs to /api/v1/schemas/fields with a sensible default type per provider.
  • Archive node form — connector picker (Select limited to S3 / GCS / Azure / SFTP / local_fs), per-connector folder-creation help text (cloud=auto-create, SFTP/local=parent must exist), and a Test connection button reusing the existing POST /api/v1/connectors/test. IR archiveNodeSchema gains an optional connectorId field (backwards compatible — runtime executor still parses destination URLs until the cross-cutting wiring lands).
  • Transform + Validate sample-row preview — collapsible widget inside both forms takes one JSON row and shows a per-op before/after diff (added/removed/changed fields highlighted) or per-rule pass/fail badge. Complex ops (aggregate, lookup_join, vector_embed, geo_resolve, sentiment_score, language_detect) and the expression op render as “preview-limited — run the pipeline” since they need server-side runtime context.
Docs: Flow Editor UI · Flow Lineage

2026-04-29 — Decisioning depth + ecosystem surfaces

A multi-pass sprint landed across 14 capability surfaces. Highlights:
  • Counterfactual training — pre-train hook augments the gradient_boosted training set with synthetic neighbors of marginal rows. See Counterfactual Training.
  • Cross-offer ranking constraints — a new constraint type with three rule shapes (channel_quota, portfolio_budget, category_cap) feeds the existing Lagrangian solver. See Lagrangian ranking.
  • KernelSHAPPOST /api/v1/decisions/:id/shap now dispatches gradient_boosted to TreeSHAP and neural_cf to KernelSHAP. See SHAP.
  • Three new fairness metrics — Gini coefficient, DeLong paired-AUC test, two-sample Kolmogorov-Smirnov. See Advanced Fairness.
  • Multi-stage four-eyes approvals + DSAR purge cron — approvals now move through a sequence of named stages with per-stage state transitions; GET /api/v1/cron/dsar-purge sweeps decision traces, interaction history, and AI attachments past the strictest tenant retention setting. See Governance four-eyes.
  • Negotiation apply-mode + multi-turn sessions — three new routes (POST /api/v1/decisions/:id/negotiate/apply, POST /api/v1/negotiate/sessions, POST /api/v1/negotiate/sessions/:id/turn) with a 7-gate apply pipeline. See Negotiation Apply-Mode.
  • 26 new connector entries added — registry expansion spanning CRM / MAP / CDP / audience-sync / helpdesk / workflow vendors. New entries ship as coming_soon form-only stubs.
  • 4 new pipeline transformsvector_embed, geo_resolve, sentiment_score, language_detect with HTTP-pointed runtime adapters under lib/flow/runtime/transforms/external-model-call.ts.
  • SCIM 2.0 + WebAuthn + SIEM audit-log shipping/scim/v2/Users endpoints, full COSE-key parse with ES256 / RS256 assertion verification, and Splunk HEC / Datadog Logs / Elastic _bulk backends gated by SSRF validation.
  • Multi-region overlayhelm/values-multi-region.yaml chart for 2-region active-active topology, plus per-tenant region routing driven by a new tenant-region pinning table.
  • In-repo SDK + CLI + Postman + MCP scaffolds — TypeScript SDK, Python SDK, npx kaireon CLI, Postman v2.1 collection, and MCP marketplace manifest under sdks/.
  • OpenAPI auto-discoverytools/scripts/gen-openapi.mjs walks every app/api/v1/**/route.ts and emits platform/public/openapi.json covering the full v1 surface.
Schema migration: platform/prisma/manual-sql/09_parity_w11_to_w19.sql (idempotent) creates 5 new tables and backfills existing single-stage approvals.

2026-04-17 — Action Insights + Reporting Platform

Four coordinated phases shipped as a single milestone: close analytics gaps, make alerts actually fire, ship a full report builder and scheduler, and deliver a C-suite executive dashboard with Export + Save-as-Report across every view.
Pilot deployment posture. This release ships as manual-only automation. The alert evaluator, report scheduler, and scheduled report runner all run through /api/cron/tick, but CRON_TOKEN and AWS EventBridge are intentionally not wired during pilot to avoid runaway LLM / notification cost. Run Now buttons, Export buttons, ad-hoc notification sends, and on-demand alert evaluation all work unconditionally. See EventBridge Setup for the optional wiring path, and the roadmap for the pilot guardrails we plan to ship before enabling automation by default.

Phase 01 — Analytics Foundation

New analytical primitives that power every downstream surface in this milestone.
  • New dashboard-data case selection_frequency — per-offer eligibleCount, scoredCount, selectedCount, selectionRate, avgRank, and rankDistribution[]. Accepts channelId, categoryId, decisionFlowId, segmentId filters.
  • New dashboard-data case anomaly_candidates — compares current vs. baseline period across acceptance rate, revenue, and degraded scoring rate; classifies severity (info / warning / critical) from z-score + absolute percent change.
  • New dashboard-data case why_not_ranked — aggregate misses per offer: scoredTooLow, filteredByContactPolicy, filteredByQualification, and beatenBy (top-5 competing offers).
  • Segment dimension added to acceptance_rate, offer_performance, offer_performance_grouped, channel_effectiveness, daily_trend, revenue_trend.
  • Enriched decision-trace JSON shapes — structured rejectionReason, rankBefore / rankAfter on scoring results.
  • Cross-decision narrative helpers that explain offer underperformance, segment coverage, and anomalies in plain language.
Docs: AI Insights · Dashboards · Dashboard Data API

Phase 02 — Notification Providers + AlertRule Execution

Pluggable notification system and live alert evaluation.
  • Pluggable notification provider interface + registry with Slack, Microsoft Teams, outbound webhook, and Ops-Email (SES) adapters.
  • New notifications tab in /settings/integrations — add / test / enable / disable / delete destinations.
  • Encrypted credential storage in the platform-setting vault (AES-256-GCM); GET endpoints return redacted configs.
  • An alert-rule evaluator compares observed vs. threshold over windowMinutes, derives severity, fans out to every destination in channels, and respects cooldownMinutes.
  • New settings page /settings/alerts — CRUD for alert rules.
  • New API surface:
    • GET /api/v1/notifications/providers and POST /api/v1/notifications/providers
    • GET /api/v1/notifications/providers/:id, PATCH /api/v1/notifications/providers/:id, DELETE /api/v1/notifications/providers/:id
    • POST /api/v1/notifications/providers/:id/test
    • POST /api/v1/notifications/send
    • GET /api/v1/alerts and POST /api/v1/alerts (alert rules — later renamed from /alert-rules to /alerts)
    • GET /api/v1/alerts/:id, PUT /api/v1/alerts/:id, DELETE /api/v1/alerts/:id
    • POST /api/cron/tick (token-authenticated; intended caller is an external scheduler such as AWS EventBridge).
Docs: Notifications · Alert Rules · Notifications API · Alerts API · Cron API · EventBridge Setup (optional during pilot)

Phase 03 — Report Builder + Scheduler

User-configurable reports with LLM narration, four output formats, and scheduled delivery through Phase 02 providers.
  • New persistent objects for report templates, report schedules, and report runs (additive migration; no changes to existing data).
  • Report data-source registry — 10 built-in sources (offer_performance, channel_effectiveness, selection_frequency, anomaly_candidates, why_not_ranked, decision_traces_summary, funnel, revenue_trend, daily_trend, budget_burn). Extension point: drop a new file in src/lib/reports/data-sources/ and register it.
  • Report format registry — built-in PDF (via @react-pdf/renderer), CSV, Markdown, HTML.
  • LLM narrative engine — uses the tenant AI provider, produces an executive summary + per-section narratives + key takeaways, and caps input at ~5000 tokens worth of rows with explicit truncation signals.
  • Report runner — loads the template, runs the configured data sources, calls the narrative engine, renders every requested format, records the run, and dispatches it to the configured destinations.
  • Full API surface:
    • GET|POST /api/v1/reports/templates, GET|PATCH|DELETE /api/v1/reports/templates/[id]
    • POST /api/v1/reports/templates/[id]/preview (transient render, no persistence)
    • POST /api/v1/reports/templates/[id]/run-now (immediate run; works without the cron)
    • GET|POST /api/v1/reports/schedules, PATCH|DELETE /api/v1/reports/schedules/[id]
    • GET /api/v1/reports/runs, GET /api/v1/reports/runs/[id]
    • GET /api/v1/reports/runs/[id]/artifacts/[format]
  • /settings/reports builder UI — compose sources, pick formats/narrative, schedule + destinations, live preview, runs history drawer.
  • /api/cron/tick extended to also process due report schedules; response JSON gains reportsEvaluated, reportsRan, reportErrors.
Docs: Reports · Report Templates · Report Schedules · First Scheduled Report guide · Reports API

Phase 04 — Executive Dashboard + Share-as-Report

C-suite-ready view and one-click sharing across every dashboard.
  • New page /dashboards/executive — LLM-narrated weekly summary, six KPI cards with period-over-period deltas and sparklines, anomaly feed (last 7 days), segment × offer heatmap, and quick-links to operational dashboards.
  • Reusable dashboard widgets:
    • Period delta — headline + Δ% vs. prior period + sparkline.
    • Anomaly feed — severity pill / metric / delta / explain button.
    • Segment-by-offer heatmap — top-10 × top-10 selection rate grid.
    • Export menu — PDF / CSV / Markdown / HTML dropdown (uses /api/v1/reports/preview under the hood; no cron required).
    • Save-as-Report — modal pre-filled from the current view; creates a report template plus a report schedule in one click.
  • Export + Save-as-Report wired into every dashboard — Business, Operations, Model Health, Data Health, Attribution (in addition to Executive).
  • Backend support for period-over-period (summary_with_comparison, model_auc_summary_with_prev, and sparklines on core metrics).
Docs: Executive Dashboard · Dashboards · Share a Dashboard guide

Earlier changes

Earlier changes are tracked in commit history — see the platform repo. Notable recent work prior to this milestone:
  • Mar–Apr 2026 — Docs remediation and sample-data corrections (connector count corrected to 24, transform count to 15, API response shapes aligned end-to-end with code).
  • Apr 2026 — Repo open-sourced under a single “Initial open source release” commit. CI and CodeQL workflows temporarily disabled pending the public repo cut-over.
  • Apr 2026 — API Explorer auto-creates an API key on first visit to the playground for streamlined onboarding.
This changelog starts with the 2026-04-17 milestone because that’s when we introduced it. For older history, consult the commit log.