Skip to main content
Reports for program execution monitoring and analytics.
Base path: /program-execution

Types

// ============================================
// EFFECTIVE STATUS
// ============================================

/**
 * Effective status includes computed pause window state.
 * - DB stores: 'scheduled' | 'running' | 'paused' | 'paused_no_credits' | 'paused_threshold' | 'completed' | 'stopped' | 'cancelled'
 * - API adds: 'paused_manual' (user paused) vs 'paused_window' (in pause window)
 */
type EffectiveStatus =
  | 'scheduled'
  | 'running'
  | 'paused_manual'      // User explicitly paused
  | 'paused_window'      // Currently in a pause window (computed at query time)
  | 'paused_no_credits'  // Auto-paused due to insufficient credits
  | 'paused_threshold'   // Auto-paused due to counter threshold reached
  | 'completed'
  | 'stopped'
  | 'cancelled';

// ============================================
// CONTACT METRICS
// ============================================

/**
 * Metrics aggregated from execution_contacts by status.
 * Represents unique contacts, not call attempts.
 */
interface ContactMetrics {
  totalContacts: number;

  // By execution_contacts.status
  pending: number;
  queued: number;
  calling: number;
  pendingRetry: number;
  completed: number;
  failed: number;
  skipped: number;

  // Derived - most important campaign metric
  reachRate: number;  // completed / totalContacts * 100
}

// ============================================
// CALL METRICS
// ============================================

/**
 * Metrics aggregated from calls by outcome.
 * Can exceed totalContacts due to retries.
 */
interface CallMetrics {
  totalCalls: number;

  // By calls.outcome
  connected: number;   // answered + voicemail (any pickup — line connected)
  answered: number;    // human answered (calls.outcome = 'completed')
  voicemail: number;
  noAnswer: number;
  busy: number;
  rejected: number;
  failed: number;

  // By calls.flowOutcome
  completed: number;       // flow ran to normal end (calls.flowOutcome = 'completed')

  // Derived
  connectionRate: number;  // connected / totalCalls * 100
  answerRate: number;      // answered / totalCalls * 100
}

// ============================================
// BILLING METRICS
// ============================================

/**
 * Billing metrics for program execution.
 */
interface BillingMetrics {
  totalCreditsUsed: number;       // Sum of all call costs
  billableCallsCount: number;     // Calls with cost > 0
  averageCostPerCall: number;     // totalCreditsUsed / billableCallsCount
}

// ============================================
// PROGRAM REPORT (Main Report)
// ============================================

interface ProgramReport {
  executionId: string;
  programId: string;
  organizationId: string;

  // Status
  status: string;                  // DB status
  effectiveStatus: EffectiveStatus;
  isInPauseWindow: boolean;

  // Timing
  scheduledStartAt: string;        // ISO date
  scheduledStopAt?: string;        // ISO date
  actualStartAt?: string;          // ISO date
  actualEndAt?: string;            // ISO date
  durationMs?: number;             // Computed: endTime - startTime

  // Metadata
  metadata: {
    flowId: string;
    flowName: string;
    audienceId: string;
    didPool: string[];
    retryStrategy: RetryStrategy;
    pauseWindows?: PauseWindows;
  };

  // Metrics
  contactMetrics: ContactMetrics;
  callMetrics: CallMetrics;
  billingMetrics: BillingMetrics;
}

// ============================================
// CALL HISTORY ITEM
// ============================================

type CallHistoryStatus = 'ongoing' | 'completed' | 'no_answer' | 'voicemail' | 'busy' | 'rejected' | 'failed';

interface CallHistoryItem {
  callId: string;
  contactId: string | null;

  // Call details
  from: string;
  to: string;
  direction: 'inbound' | 'outbound';
  attempt: number;

  // Status
  status: CallHistoryStatus;
  outcome: string | null;

  // Timing
  startedAt: string;          // ISO date
  endedAt?: string;           // ISO date
  durationMs?: number;

  // Billing
  totalCost?: number;         // Total cost for this call
}

// ============================================
// ATTEMPT BREAKDOWN
// ============================================

interface AttemptBreakdown {
  attemptNumber: number;       // 1, 2, 3... or 0 for total

  // Counts
  callsAttempted: number;
  callsConnected: number;     // answered + voicemail (any pickup)
  callsAnswered: number;      // human answered
  callsVoicemail: number;
  callsNoAnswer: number;
  callsFailed: number;
  callsCompleted: number;     // flow ran to normal end

  // Percentages (relative to callsAttempted for this attempt)
  connectedPercentage: number;
  answeredPercentage: number;
  voicemailPercentage: number;
  noAnswerPercentage: number;
  completedPercentage: number;

  // Metrics
  averageDurationMs: number;
}

interface EventBreakdownResponse {
  perAttempt: AttemptBreakdown[];
  total: AttemptBreakdown;
}

// ============================================
// NODE EXECUTION STATS
// ============================================

/**
 * Execution statistics for a single node.
 *
 * NOTE: This is NOT a funnel because call flows are directed graphs
 * with branches (DTMF/condition) and cycles (loops back to previous nodes).
 * We report unique calls per node and total executions, not drop-off rates.
 */
interface NodeExecutionStat {
  nodeId: string;
  nodeType: string;
  nodeLabel?: string;

  // Unique calls that executed this node (at least once)
  uniqueCalls: number;

  // Total executions including loops/retries (uniqueCalls <= totalExecutions)
  totalExecutions: number;

  // % of calls that reached this node: uniqueCalls / totalCalls * 100
  percentage: number;

  // DTMF-specific (only for nodeType === 'dtmf')
  dtmfBreakdown?: Record<string, number>;      // e.g., { "1": 50, "2": 30 }
  dtmfPercentages?: Record<string, number>;    // e.g., { "1": 62.5, "2": 37.5 }
}

interface NodeExecutionStatsResponse {
  nodes: NodeExecutionStat[];

  // Summary
  totalCalls: number;
  completedCalls: number;      // flow_outcome = 'completed'
  completionRate: number;      // completedCalls / totalCalls * 100
}

// ============================================
// PROGRAM BILLING REPORT
// ============================================

/**
 * Cost breakdown by node type (e.g., all DIAL nodes, all RECORD nodes)
 */
interface NodeTypeCostBreakdown {
  nodeType: string;           // 'dial' | 'record' | etc.
  totalCost: number;          // Total cost for all nodes of this type
  executionCount: number;     // Number of executions
  totalDurationMs: number | null;
}

/**
 * Top costly call entry
 */
interface TopCostlyCall {
  callId: string;
  contactId: string | null;
  totalCost: number;
  durationMs: number | null;
}

/**
 * Detailed billing report for a program execution
 */
interface ProgramBillingReport {
  executionId: string;

  summary: {
    totalCreditsUsed: number;
    billableCallsCount: number;
    averageCostPerCall: number;
  };

  // Cost breakdown by node type
  byNodeType: NodeTypeCostBreakdown[];

  // Top 10 most expensive calls
  topCostlyCalls: TopCostlyCall[];
}

// ============================================
// PAGINATION
// ============================================

interface PaginationMeta {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
}

interface PaginatedResponse<T> {
  data: T[];
  meta: PaginationMeta;
}

Get main program execution report with metrics

// Request
// GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report

// Response 200
{
  "executionId": "770e8400-e29b-41d4-a716-446655440003",
  "programId": "550e8400-e29b-41d4-a716-446655440000",
  "organizationId": "660e8400-e29b-41d4-a716-446655440001",

  "status": "running",
  "effectiveStatus": "paused_window",
  "isInPauseWindow": true,

  "scheduledStartAt": "2025-12-20T09:00:00.000Z",
  "scheduledStopAt": "2025-12-20T18:00:00.000Z",
  "actualStartAt": "2025-12-20T09:00:05.000Z",
  "durationMs": 14400000,

  "metadata": {
    "flowId": "03418632-e6da-451e-875f-23b55f938e3e",
    "flowName": "Holiday Campaign Flow",
    "audienceId": "550e8400-e29b-41d4-a716-446655440001",
    "didPool": ["+212500000001", "+212500000002"],
    "retryStrategy": {
      "type": "fixed_delay",
      "delayMinutes": 30,
      "maxRetries": 2
    },
    "pauseWindows": {
      "monday": [
        { "startAt": { "hour": 12, "minute": 0 }, "endAt": { "hour": 14, "minute": 0 } }
      ]
    }
  },

  "contactMetrics": {
    "totalContacts": 5000,
    "pending": 1000,
    "queued": 50,
    "calling": 25,
    "pendingRetry": 200,
    "completed": 3500,
    "failed": 200,
    "skipped": 25,
    "reachRate": 70.0
  },

  "callMetrics": {
    "totalCalls": 6200,
    "connected": 4000,
    "answered": 3500,
    "voicemail": 500,
    "noAnswer": 1500,
    "busy": 400,
    "rejected": 100,
    "failed": 200,
    "completed": 3200,
    "connectionRate": 64.52,
    "answerRate": 56.45
  },

  "billingMetrics": {
    "totalCreditsUsed": 4650.00,
    "billableCallsCount": 4000,
    "averageCostPerCall": 1.1625
  }
}
Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Get paginated call history for the execution

// Request
// GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/calls?page=1&limit=10

// Response 200
{
  "data": [
    {
      "callId": "1cbdf482-4dac-41ed-a430-2e311add389d",
      "contactId": "6fd21c0b-bf05-407e-bcd7-9f9aab7ab91e",
      "from": "+212500000001",
      "to": "+212612345678",
      "direction": "outbound",
      "attempt": 1,
      "status": "completed",
      "outcome": "completed",
      "startedAt": "2025-12-20T09:15:00.000Z",
      "endedAt": "2025-12-20T09:15:45.000Z",
      "durationMs": 45000,
      "totalCost": 3.375
    },
    {
      "callId": "eae26ac8-be39-490b-b9ed-a82e7f4048f6",
      "contactId": "7fd21c0b-bf05-407e-bcd7-9f9aab7ab92f",
      "from": "+212500000002",
      "to": "+212698765432",
      "direction": "outbound",
      "attempt": 2,
      "status": "no_answer",
      "outcome": "no_answer",
      "startedAt": "2025-12-20T09:45:00.000Z",
      "endedAt": "2025-12-20T09:45:30.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 6200,
    "totalPages": 620
  }
}
Status Mapping: Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Get event breakdown per attempt number

// Request
// GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/breakdown

// Response 200
{
  "perAttempt": [
    {
      "attemptNumber": 1,
      "callsAttempted": 5000,
      "callsConnected": 3200,
      "callsAnswered": 2800,
      "callsVoicemail": 400,
      "callsNoAnswer": 1200,
      "callsFailed": 600,
      "callsCompleted": 2500,
      "connectedPercentage": 64.0,
      "answeredPercentage": 56.0,
      "voicemailPercentage": 8.0,
      "noAnswerPercentage": 24.0,
      "completedPercentage": 50.0,
      "averageDurationMs": 42000
    },
    {
      "attemptNumber": 2,
      "callsAttempted": 1200,
      "callsConnected": 800,
      "callsAnswered": 700,
      "callsVoicemail": 100,
      "callsNoAnswer": 300,
      "callsFailed": 100,
      "callsCompleted": 650,
      "connectedPercentage": 66.67,
      "answeredPercentage": 58.33,
      "voicemailPercentage": 8.33,
      "noAnswerPercentage": 25.0,
      "completedPercentage": 54.17,
      "averageDurationMs": 38000
    }
  ],
  "total": {
    "attemptNumber": 0,
    "callsAttempted": 6200,
    "callsConnected": 4000,
    "callsAnswered": 3500,
    "callsVoicemail": 500,
    "callsNoAnswer": 1500,
    "callsFailed": 700,
    "callsCompleted": 3150,
    "connectedPercentage": 64.52,
    "answeredPercentage": 56.45,
    "voicemailPercentage": 8.06,
    "noAnswerPercentage": 24.19,
    "completedPercentage": 50.81,
    "averageDurationMs": 40500
  }
}
Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Get node execution statistics

Important: This is NOT a traditional funnel. Call flows are directed graphs with:
  • Branches: DTMF nodes and condition nodes route to different paths
  • Cycles: Flows can loop back to previous nodes
We report uniqueCalls (calls that reached the node at least once) and totalExecutions (including loops), not drop-off rates.
// Request
// GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/node-stats

// Response 200
{
  "nodes": [
    {
      "nodeId": "dial-1",
      "nodeType": "dial",
      "nodeLabel": "Dial Contact",
      "uniqueCalls": 6200,
      "totalExecutions": 6200,
      "percentage": 100.0
    },
    {
      "nodeId": "play-welcome",
      "nodeType": "play",
      "nodeLabel": "Welcome Message",
      "uniqueCalls": 4000,
      "totalExecutions": 4000,
      "percentage": 64.52
    },
    {
      "nodeId": "dtmf-menu",
      "nodeType": "dtmf",
      "nodeLabel": "Main Menu",
      "uniqueCalls": 3800,
      "totalExecutions": 4200,
      "percentage": 61.29,
      "dtmfBreakdown": {
        "1": 2000,
        "2": 1200,
        "3": 600
      },
      "dtmfPercentages": {
        "1": 52.63,
        "2": 31.58,
        "3": 15.79
      }
    },
    {
      "nodeId": "play-option1",
      "nodeType": "play",
      "nodeLabel": "Option 1 Info",
      "uniqueCalls": 2000,
      "totalExecutions": 2000,
      "percentage": 32.26
    },
    {
      "nodeId": "hangup-1",
      "nodeType": "hangup",
      "nodeLabel": "End Call",
      "uniqueCalls": 3500,
      "totalExecutions": 3500,
      "percentage": 56.45
    }
  ],
  "totalCalls": 6200,
  "completedCalls": 3500,
  "completionRate": 56.45
}
Why uniqueCalls vs totalExecutions:
  • If a flow has a “retry menu” that loops back to the DTMF node, a single call might execute that node 3 times
  • uniqueCalls: 100 means 100 different calls reached this node
  • totalExecutions: 150 means those 100 calls executed the node 150 times total
Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Get detailed billing report for the execution

// Request
// GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/billing

// Response 200
{
  "executionId": "770e8400-e29b-41d4-a716-446655440003",

  "summary": {
    "totalCreditsUsed": 4650.00,
    "billableCallsCount": 4000,
    "averageCostPerCall": 1.1625
  },

  "byNodeType": [
    {
      "nodeType": "dial",
      "totalCost": 4500.00,
      "executionCount": 6200,
      "totalDurationMs": 186000000
    },
    {
      "nodeType": "record",
      "totalCost": 150.00,
      "executionCount": 500,
      "totalDurationMs": 2631579
    }
  ],

  "topCostlyCalls": [
    {
      "callId": "1cbdf482-4dac-41ed-a430-2e311add389d",
      "contactId": "6fd21c0b-bf05-407e-bcd7-9f9aab7ab91e",
      "totalCost": 15.75,
      "durationMs": 210000
    },
    {
      "callId": "eae26ac8-be39-490b-b9ed-a82e7f4048f6",
      "contactId": "7fd21c0b-bf05-407e-bcd7-9f9aab7ab92f",
      "totalCost": 12.30,
      "durationMs": 164000
    }
  ]
}
Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Export program execution summary as a downloadable XLSX o…

Contains 3 sheets (XLSX) or sections (CSV): Summary, Breakdown, Node Stats.
GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/export?format=xlsx
→ Downloads report-770e8400-e29b-41d4-a716-446655440003.xlsx
XLSX Sheets: Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Export detailed call-level data as a downloadable XLSX or…

One row per call attempt. Columns are: fixed fields + custom attributes + dynamic node columns.
GET /program-execution/770e8400-e29b-41d4-a716-446655440003/report/calls/export?format=csv
→ Downloads calls-770e8400-e29b-41d4-a716-446655440003.csv
Column Layout:
[Fixed Columns] → [Custom Attribute Columns] → [Dynamic Node Columns]
Fixed Columns (18): Custom Attribute Columns (dynamic, per organization): One column per custom attribute defined for the organization (custom_attributes table), using displayName as the header. Values from contacts_custom_attributes for each contact. Empty if the contact has no value for that attribute. Dynamic Node Columns (per flow node): Columns generated from the flow graph. Skipped node types: answer, hangup.
Node TypeColumns Generated
dial[label]: Result
play[label]: Result
say[label]: Result
condition[label]: Result
sms[label]: Status, [label]: Message, [label]: Parts
update_contact[label]: Attribute, [label]: Current Value
dtmf[label]: Input, [label]: Branch
collect_audio[label]: Result, [label]: Recording ID, [label]: Transcription
record[label]: Result, [label]: Recording ID, [label]: Transcription
set_variable[label]: Value
  • Result: The execution path output (e.g. onAnswer, onComplete, branch_1)
  • Input: DTMF digits pressed by caller (from call_events)
  • Branch: Which DTMF branch was taken (from execution path)
  • Recording ID: Recording identifier (from call_recordings)
  • Transcription: Transcribed text of the recording (from call_recordings.transcriptionText)
  • Status: SMS delivery status (from sms_logs)
  • Message: Rendered SMS message body (from sms_logs.renderedMessage)
  • Parts: Number of SMS parts/segments (from sms_logs.smsParts)
  • Attribute: The contact attribute name being updated (from node.config.attributeName)
  • Current Value: Current value of the attribute for the contact (from contacts_custom_attributes)
  • Value: Final variable value (from flowExecutionData.finalVariables)
Sort Order: Contact last name ASC (nulls last), then attempt number ASC. Error Responses:
CodeErrorDescription
404NotFoundExceptionExecution not found

Notes

Effective Status Logic

if (dbStatus === 'paused') {
  return 'paused_manual';
}

if (dbStatus === 'running' && isInPauseWindow) {
  return 'paused_window';
}

// paused_no_credits and paused_threshold pass through as-is
return dbStatus;
Pause window check is computed at query time using the execution’s pauseWindows configuration. paused_no_credits and paused_threshold are stored directly in the DB and returned as-is.

Contact vs Call Metrics

Example: 1000 contacts with 2 retries = 1000 in ContactMetrics, up to 3000 in CallMetrics.

Node Stats vs Funnel

Traditional funnels assume linear flow:
Step 1 (100%) → Step 2 (80%) → Step 3 (60%)
Call flows are graphs with branches and cycles:
          ┌─→ Option1 ─→ Hangup
DTMF ─────┼─→ Option2 ─→ Hangup
          └─→ Retry ────┘ (loops back)
Therefore we report:
  • percentage: What % of total calls reached this node (not % of previous node)
  • totalExecutions: Includes loops (same call executing node multiple times)