Files
helix-engage/docs/superpowers/specs/2026-03-31-rules-engine-design.md
saridsa2 b90740e009 feat: rules engine — priority config UI + worklist scoring
- Rules engine spec v2 (priority vs automation rules distinction)
- Priority Rules settings page with weight sliders, SLA config, campaign/source weights
- Collapsible config sections with dynamic headers
- Live worklist preview panel with client-side scoring
- AI assistant panel (collapsible) with rules-engine-specific system prompt
- Worklist panel: score display with SLA status dots, sort by score
- Scoring library (scoring.ts) for client-side preview computation
- Sidebar: Rules Engine nav item under Configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:20:59 +05:30

15 KiB
Raw Blame History

Rules Engine — Design Spec (v2)

Date: 2026-03-31 (revised 2026-04-01) Status: Approved Phase: 1 (Engine + Storage + API + Priority Rules UI + Worklist Integration)


Overview

A configurable rules engine that governs how leads flow through the hospital's call center — which leads get called first, which agent handles them, when to escalate, and when to mark them lost. Each hospital defines its own rules. No code changes needed to change behavior.

Product pitch: "Your hospital defines the rules, the call center follows them automatically."


Two Rule Types

The engine supports two categories of rules, each with different behavior and UI:

Priority Rules — "Who gets called first?"

  • Configures worklist ranking via weights, SLA curves, campaign modifiers
  • Computed at request time — scores are ephemeral, not persisted to entities
  • Time-sensitive (SLA elapsed changes every minute — can't be persisted)
  • Supervisor sees: weight sliders, SLA thresholds, campaign weights, live worklist preview
  • No draft/publish needed — changes affect ranking immediately

Automation Rules — "What should happen automatically?"

  • Triggers durable actions when conditions are met: field updates, assignments, notifications
  • Writes back to entities via platform GraphQL mutations (e.g., set lead.priority = HIGH)
  • Event-driven (fires on lead.created, call.missed, etc.) or scheduled (every 5m)
  • Supervisor sees: if-this-then-that condition builder with entity/field selectors
  • Draft/publish workflow — rules don't affect live data until published
  • Sub-types: Assignment, Escalation, Lifecycle
Aspect Priority Rules Automation Rules
When On worklist request On entity event / on schedule
Effect Ephemeral score for ranking Durable entity mutation
Persisted? No (recomputed each request) Yes (writes to platform)
Draft/publish? No (immediate) Yes
UI Sliders + live preview Condition builder + draft/publish

Architecture

Self-contained NestJS module inside helix-engage-server (sidecar). Designed for extraction into a standalone microservice when needed.

helix-engage-server/src/rules-engine/
├── rules-engine.module.ts          # NestJS module (self-contained)
├── rules-engine.service.ts         # Core: json-rules-engine wrapper
├── rules-engine.controller.ts      # REST API: CRUD + evaluate + config
├── rules-storage.service.ts        # Redis (hot) + JSON file (backup)
├── types/
│   ├── rule.types.ts               # Rule schema (priority + automation)
│   ├── fact.types.ts               # Fact definitions + computed facts
│   └── action.types.ts             # Action handler interface
├── facts/
│   ├── lead-facts.provider.ts      # Lead/campaign data facts
│   ├── call-facts.provider.ts      # Call/SLA data facts (+ computed: ageMinutes, slaElapsed)
│   └── agent-facts.provider.ts     # Agent availability facts
├── actions/
│   ├── score.action.ts             # Priority scoring action
│   ├── assign.action.ts            # Lead-to-agent assignment (stub)
│   ├── escalate.action.ts          # SLA breach alerts (stub)
│   └── update.action.ts            # Update entity field (stub)
├── consumers/
│   └── worklist.consumer.ts        # Applies scoring rules to worklist
└── templates/
    └── hospital-starter.json       # Pre-built rule set for new hospitals

Dependencies

  • json-rules-engine (npm) — rule evaluation
  • Redis — active rule storage, score cache
  • Platform GraphQL — fact data (leads, calls, campaigns, agents)
  • No imports from other sidecar modules except via constructor injection

Communication

  • Own Redis namespace: rules:*
  • Own route prefix: /api/rules/*
  • Other modules call RulesEngineService.evaluate() — they don't import internals

Fact System

Design Principle: Entity-Driven Facts

Facts should ultimately be driven by entity metadata from the platform — adding a field to an entity automatically makes it available as a fact. This is the long-term goal.

Phase 1: Curated Facts + Computed Facts

For Phase 1, facts are curated (hardcoded providers) with two categories:

Entity field facts — direct field values from platform entities:

  • lead.source, lead.status, lead.campaignId, etc.
  • call.direction, call.status, call.callbackStatus, etc.
  • agent.status, agent.skills, etc.

Computed facts — derived values that don't exist as entity fields:

  • lead.ageMinutes — computed from createdAt
  • call.slaElapsedPercent — computed from createdAt + task type SLA
  • call.slaBreached — computed from slaElapsedPercent > 100
  • call.taskType — inferred from call data (missed_call, follow_up, campaign_lead, etc.)

Phase 2: Metadata-Driven Discovery

  • Query platform metadata API to discover entities and fields dynamically
  • Each field's type (NUMBER, TEXT, SELECT, BOOLEAN) drives:
    • Available operators in the condition builder UI
    • Input type (slider, dropdown with enum values, text, toggle)
  • Computed facts remain registered in code alongside metadata-driven facts

Rule Schema

type RuleType = 'priority' | 'automation';

type Rule = {
    id: string;                          // UUID
    ruleType: RuleType;                  // Priority or Automation
    name: string;                        // Human-readable
    description?: string;                // BA-friendly explanation
    enabled: boolean;                    // Toggle on/off without deleting
    priority: number;                    // Evaluation order (lower = first)

    trigger: RuleTrigger;                // When to evaluate
    conditions: RuleConditionGroup;      // What to check
    action: RuleAction;                  // What to do

    // Automation rules only
    status?: 'draft' | 'published';      // Draft/publish workflow

    metadata: {
        createdAt: string;
        updatedAt: string;
        createdBy: string;
        category: RuleCategory;
        tags?: string[];
    };
};

type RuleTrigger =
    | { type: 'on_request'; request: 'worklist' | 'assignment' }
    | { type: 'on_event'; event: string }
    | { type: 'on_schedule'; interval: string }
    | { type: 'always' };

type RuleCategory =
    | 'priority'        // Worklist scoring (Priority Rules)
    | 'assignment'      // Lead/call routing to agent (Automation)
    | 'escalation'      // SLA breach handling (Automation)
    | 'lifecycle'       // Lead status transitions (Automation)
    | 'qualification';  // Lead quality scoring (Automation)

type RuleConditionGroup = {
    all?: (RuleCondition | RuleConditionGroup)[];
    any?: (RuleCondition | RuleConditionGroup)[];
};

type RuleCondition = {
    fact: string;                        // Fact name
    operator: RuleOperator;
    value: any;
    path?: string;                       // JSON path for nested facts
};

type RuleOperator =
    | 'equal' | 'notEqual'
    | 'greaterThan' | 'greaterThanInclusive'
    | 'lessThan' | 'lessThanInclusive'
    | 'in' | 'notIn'
    | 'contains' | 'doesNotContain'
    | 'exists' | 'doesNotExist';

type RuleAction = {
    type: RuleActionType;
    params: ScoreActionParams | AssignActionParams | EscalateActionParams | UpdateActionParams;
};

type RuleActionType = 'score' | 'assign' | 'escalate' | 'update' | 'notify';

// Score action params (Priority Rules)
type ScoreActionParams = {
    weight: number;              // 0-10 base weight
    slaMultiplier?: boolean;     // Apply SLA urgency curve
    campaignMultiplier?: boolean; // Apply campaign weight
};

// Assign action params (Automation Rules — stub)
type AssignActionParams = {
    agentId?: string;
    agentPool?: string[];
    strategy: 'specific' | 'round-robin' | 'least-loaded' | 'skill-based';
};

// Escalate action params (Automation Rules — stub)
type EscalateActionParams = {
    channel: 'toast' | 'notification' | 'sms' | 'email';
    recipients: 'supervisor' | 'agent' | string[];
    message: string;
    severity: 'warning' | 'critical';
};

// Update action params (Automation Rules — stub)
type UpdateActionParams = {
    entity: string;
    field: string;
    value: any;
};

Priority Rules — Scoring System

Formula

finalScore = baseScore × slaMultiplier × campaignMultiplier

Base Score

Determined by the rule's weight param (0-10). Multiple rules can fire for the same item — scores are summed.

SLA Multiplier (time-sensitive, computed at request time)

if slaElapsed <= 100%:  multiplier = (slaElapsed / 100) ^ 1.6
if slaElapsed > 100%:   multiplier = 1.0 + (excess × 0.05)

Non-linear curve — urgency accelerates as deadline approaches. Continues increasing past breach.

Campaign Multiplier

campaignWeight (0-10) / 10 × sourceWeight (0-10) / 10

IVF(9) × WhatsApp(9) = 0.81. Health(7) × Instagram(5) = 0.35.

Priority Config (supervisor-editable)

type PriorityConfig = {
    taskWeights: Record<string, { weight: number; slaMinutes: number }>;
    campaignWeights: Record<string, number>;   // campaignId → 0-10
    sourceWeights: Record<string, number>;     // leadSource → 0-10
};

// Default config (from hospital starter template)
const DEFAULT_PRIORITY_CONFIG = {
    taskWeights: {
        missed_call:   { weight: 9, slaMinutes: 720 },    // 12 hours
        follow_up:     { weight: 8, slaMinutes: 1440 },   // 1 day
        campaign_lead: { weight: 7, slaMinutes: 2880 },   // 2 days
        attempt_2:     { weight: 6, slaMinutes: 1440 },
        attempt_3:     { weight: 4, slaMinutes: 2880 },
    },
    campaignWeights: {},   // Empty = no campaign multiplier
    sourceWeights: {
        WHATSAPP: 9, PHONE: 8, FACEBOOK_AD: 7, GOOGLE_AD: 7,
        INSTAGRAM: 5, WEBSITE: 7, REFERRAL: 6, WALK_IN: 5, OTHER: 5,
    },
};

This config is what the Priority Rules UI edits via sliders. Under the hood, each entry generates a json-rules-engine rule.


Priority Rules UI (Supervisor Settings)

Layout

Settings page → "Priority" tab with three sections:

Section 1: Task Type Weights

Task Type Weight (slider 0-10) SLA (input)
Missed Calls ████████░░ 9 12h
Follow-ups ███████░░░ 8 1d
Campaign Leads ██████░░░░ 7 2d
2nd Attempt █████░░░░░ 6 1d
3rd Attempt ███░░░░░░░ 4 2d

Section 2: Campaign Weights Shows existing campaigns with weight sliders. Default 5.

Campaign Weight
IVF Awareness ████████░░ 9
Health Checkup ██████░░░░ 7
Cancer Screening ███████░░░ 8

Section 3: Source Weights

Source Weight
WhatsApp ████████░░ 9
Phone ███████░░░ 8
Facebook Ad ██████░░░░ 7
... ...

Section 4: Live Preview Shows the current worklist re-ranked with the configured weights. As supervisor adjusts sliders, preview updates in real-time (client-side computation using the same scoring formula).

Components

  • Untitled UI Slider (if available) or custom range input
  • Untitled UI Toggle for enable/disable per task type
  • Untitled UI Tabs for Priority / Automations
  • Score badges showing computed values in preview

Storage

Redis Keys

rules:config                    # JSON array of all Rule objects
rules:priority-config           # PriorityConfig JSON (slider values)
rules:config:backup_path        # Path to JSON backup file
rules:scores:{itemId}           # Cached base score per worklist item
rules:scores:version            # Incremented on rule change (invalidates all scores)
rules:eval:log:{ruleId}         # Last evaluation result (debug)

JSON File Backup

On every rule/config change:

  1. Write to Redis
  2. Persist to data/rules-config.json + data/priority-config.json in sidecar working directory
  3. On sidecar startup: if Redis is empty, load from JSON files

API Endpoints

Priority Config (used by UI sliders)

GET    /api/rules/priority-config          # Get current priority config
PUT    /api/rules/priority-config          # Update priority config (slider values)
POST   /api/rules/priority-config/preview  # Preview scoring with modified config

Rule CRUD (for automation rules)

GET    /api/rules                    # List all rules
GET    /api/rules/:id                # Get single rule
POST   /api/rules                    # Create rule
PUT    /api/rules/:id                # Update rule
DELETE /api/rules/:id                # Delete rule
PATCH  /api/rules/:id/toggle         # Enable/disable
POST   /api/rules/reorder            # Change evaluation order

Evaluation

POST   /api/rules/evaluate           # Evaluate rules against provided facts

Templates

GET    /api/rules/templates           # List available rule templates
POST   /api/rules/templates/:id/apply # Apply a template (creates rules + config)

Worklist Integration

Current Flow

GET /api/worklist → returns { missedCalls, followUps, marketingLeads } → frontend sorts by priority + createdAt

New Flow

GET /api/worklist → fetch 3 arrays → score each item via RulesEngineService → return with scores → frontend sorts by score

Response Change

Each worklist item gains:

{
    ...existingFields,
    score: number;              // Computed priority score
    scoreBreakdown: {           // Explainability
        baseScore: number;
        slaMultiplier: number;
        campaignMultiplier: number;
        rulesApplied: string[]; // Rule names that fired
    };
    slaStatus: 'low' | 'medium' | 'high' | 'critical';
    slaElapsedPercent: number;
}

Frontend Changes

  • Worklist sorts by score descending instead of hardcoded priority
  • SLA status dot (green/amber/red/dark-red) replaces priority badge
  • Tooltip on score shows breakdown ("IVF campaign ×0.81, Missed call weight 9, SLA 72% elapsed")

Hospital Starter Template

Pre-configured priority config + automation rules for a typical hospital. Applied on first setup via POST /api/rules/templates/hospital-starter/apply.

Creates:

  1. PriorityConfig with default task/campaign/source weights
  2. Scoring rules in rules:config matching the config
  3. One escalation rule stub (SLA breach → supervisor notification)

Scope Boundaries

In scope (Phase 1 — Friday):

  • json-rules-engine integration in sidecar
  • Rule schema with ruleType: 'priority' | 'automation' distinction
  • Curated fact providers (lead, call, agent) with computed facts
  • Score action handler (full) + assign/escalate/update stubs
  • Redis storage + JSON backup
  • PriorityConfig CRUD + preview endpoints
  • Rule CRUD API endpoints
  • Worklist consumer (scoring integration)
  • Hospital starter template
  • Priority Rules UI — supervisor settings page with weight sliders, SLA config, live preview
  • Frontend worklist changes (score display, SLA dots, breakdown tooltip)

Out of scope (Phase 2+):

  • Automation Rules UI (condition builder with entity/field selectors)
  • Metadata-driven fact discovery from platform API
  • Assignment/escalation/update action handlers (stubs in Phase 1)
  • Event-driven rule evaluation (on_event triggers)
  • Scheduled rule evaluation (on_schedule triggers)
  • Draft/publish workflow for automation rules
  • Multi-tenant rule isolation