Communication Scheduler
Orchestrates outbound call scheduling at scale. Accepts call schedules with configurable time windows, retry policies, and concurrency limits; stages contacts via JSON or CSV upload; dispatches outbound calls through Quartz JDBC jobs; and resumes after each call completes via Redis pub/sub.
| Attribute |
Value |
| Image |
communication-services/communication-scheduler |
| Runtime |
Spring Boot 4.0.5 / Java 25 |
| Namespace |
nexivo |
| Replicas |
3 |
| Database |
scheduled_communications (PostgreSQL) |
Tech Stack
| Component |
Technology |
| Framework |
Spring Boot 4.0.5 |
| Language |
Java 25 |
| Database |
PostgreSQL |
| Queuing |
Redis (ZSET + pub/sub) |
| Job Scheduling |
Quartz Scheduler (JDBC-backed) |
| Batch Import |
Spring Batch |
| Script Templating |
FreeMarker |
| HTTP Clients |
Feign → call-service, contact-service |
Core Entities
CallSchedule
| Field |
Type |
Notes |
id |
Long |
Auto-increment PK |
scheduleId |
UUID |
External identifier |
name |
String |
Display name |
status |
Enum |
DRAFT → SCHEDULED → RUNNING → PASSED → COMPLETED |
outboundTrunkId |
UUID |
References Phone Number Service trunk |
purpose |
String |
Call purpose description |
script |
String |
FreeMarker template |
greetings |
String |
Opening greeting |
retryAttempts |
Integer |
Max retry count |
retryInterval |
String |
ISO 8601 duration (e.g. PT10M) |
scheduledAt |
OffsetDateTime |
Fire time |
callingHoursStart |
LocalTime |
Daily window start |
callingHoursEnd |
LocalTime |
Daily window end |
timezone |
String |
IANA timezone identifier |
concurrentCalls |
Integer |
1–10 simultaneous calls |
dispositionGroups |
List\<UUID> |
JSONB — disposition group filter |
routeConfig |
JSONB |
{ role, aiAgentId, teamId, transferable } |
callAfterFormula |
JSONB |
Per-contact window start formula |
callBeforeFormula |
JSONB |
Per-contact window end formula |
respectDnb |
Boolean |
Honour Do Not Disturb flag |
Relations: one-to-many CsvUpload, one-to-many ScheduledCall.
ScheduledCall
| Field |
Type |
Notes |
id |
UUID |
PK |
contactId |
UUID |
Reference to contact |
callSchedule |
FK |
Parent CallSchedule |
variables |
Map\<String,String> |
JSONB — FreeMarker substitution values |
attempts |
Integer |
Retry counter |
status |
Enum |
SCHEDULED / IN_PROGRESS / DO_NOT_DISTURB / ANSWERED / MISSED / BUSY / FAILED |
callAfter |
OffsetDateTime |
Computed window start |
callBefore |
OffsetDateTime |
Computed window end |
callId |
UUID |
Call ID returned by call-service |
lastAttemptedAt |
OffsetDateTime |
Timestamp of last dispatch attempt |
Inserts are idempotent (ON CONFLICT DO NOTHING).
CsvUpload
| Field |
Type |
Notes |
id |
UUID |
PK |
callSchedule |
FK |
Parent CallSchedule |
fileName |
String |
Original filename |
fileContent |
byte[] |
Raw file bytes |
createdAt |
OffsetDateTime |
Upload timestamp |
REST API
CallSchedule — /call-schedules
| Method |
Path |
Description |
POST |
/ |
Create a new call schedule |
GET |
/{id} |
Retrieve schedule (detail projection) |
GET |
/ |
Search and paginate schedules |
PUT |
/{id} |
Full update |
DELETE |
/{id} |
Delete schedule (cascades to contacts) |
GET |
/{id}/triggers |
List Quartz triggers for this schedule |
POST |
/{id}/trigger |
Immediate manual trigger |
DELETE |
/{id}/triggers |
Pause / unschedule all triggers |
PATCH |
/{id}/schedule |
Update scheduledAt + timezone |
PATCH |
/{id}/instructions |
Update script, purpose, greetings |
PATCH |
/{id}/contact-list |
Update contact list identifier |
GET |
/{id}/disposition-groups |
Get disposition group UUID list |
PATCH |
/{id}/disposition-groups |
Replace disposition group list |
Scheduled Calls — /call-schedules/{scheduleId}/scheduled-calls
| Method |
Path |
Description |
POST |
/ |
Add contacts as JSON list |
POST |
/uploads |
Upload CSV file (multipart) |
POST |
/uploads/{uploadId}/submit |
Kick off Spring Batch import job |
GET |
/uploads/{uploadId}/jobs/{jobExecutionId} |
Poll batch job status |
GET |
/ |
Paginated list of scheduled calls |
DELETE |
/{scheduledCallId} |
Remove individual scheduled call |
GET |
/{scheduledCallId}/script |
Render FreeMarker script with contact variables |
Execution Flow
sequenceDiagram
participant Client
participant API as CallScheduleController
participant Listener as CallScheduleChangeListener
participant Quartz as Quartz JobService
participant Job as CallScheduleJob
participant Redis as Redis ZSET
participant Feign as call-service (Feign)
participant Sub as RedisCallEndListener
Client->>API: POST /call-schedules
API->>API: Persist CallSchedule (status=SCHEDULED)
API->>Listener: CallScheduleChange event (after-commit)
Listener->>Quartz: scheduleJob(scheduleId, scheduledAt, timezone)
Note over Quartz: At fire time...
Quartz->>Job: execute CallScheduleJob
Job->>Redis: ZADD scheduled-calls:{scheduleId} (eligible calls)
Note over Job,Redis: Windowed: callAfter<=now<callBefore<br/>Unrestricted: no window constraints
Job->>Redis: ZPOPMIN N (concurrentCalls)
loop For each popped call
Job->>Feign: dispatchCall → call-service (outbound call)
Feign-->>Job: callId
Job->>Job: Update ScheduledCall: status=IN_PROGRESS, callId
end
Note over Sub: Call ends in call-service...
Sub->>Sub: Receive Redis pub/sub call-end event
Sub->>Sub: Publish ScheduledCallEnd event
Sub->>Job: triggerCalls(scheduleId, 1)
Job->>Redis: ZPOPMIN 1
alt More calls in ZSET
Job->>Feign: dispatchCall → call-service
else ZSET empty + windowed calls exhausted
Job->>Quartz: reschedule trigger to next callAfter time
else ZSET empty + no more calls
Job->>Job: Transition CallSchedule → COMPLETED
end
Window Logic
callAfterFormula and callBeforeFormula compute per-contact OffsetDateTime windows at import time.
- At job execution, windowed calls (where
callAfter <= now < callBefore) are loaded into the ZSET first.
- If the windowed queue is exhausted before the next window opens, the Quartz trigger is rescheduled to the earliest next
callAfter time.
- Unrestricted calls (no formula set) are loaded as a fallback when windowed calls are exhausted.
CSV Import (Spring Batch)
| Setting |
Value |
| Chunk size |
50 |
| Skip limit |
100 |
| Skippable exceptions |
IllegalArgumentException, FeignException |
Processing steps:
- Reader — reads CSV rows from stored
CsvUpload.fileContent.
- Processor — maps CSV columns to a
ScheduledCall; resolves the contact via ContactService (by phone or email) or creates a new contact; applies callAfter / callBefore formulas to compute the time window.
- Writer — batch-saves
ScheduledCall records with ON CONFLICT DO NOTHING to ensure idempotency.
Job execution state is tracked in spring_batch_* tables. Status can be polled via GET /uploads/{uploadId}/jobs/{jobExecutionId}.
Script Rendering (FreeMarker)
GET /call-schedules/{scheduleId}/scheduled-calls/{id}/script renders the CallSchedule.script FreeMarker template for a specific contact.
The FreeMarker context receives:
contact — the resolved contact object
variables — the Map<String,String> stored on the ScheduledCall
On render failure the raw (unrendered) template string is returned, so the API never errors on a template syntax issue.
Key Configuration
| Property |
Description |
spring.datasource.url |
jdbc:postgresql://…/scheduled_communications |
spring.quartz.job-store-type |
jdbc |
spring.quartz.properties.org.quartz.jobStore.tablePrefix |
public.qrtz_ |
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass |
PostgreSQLDelegate |
| Quartz HikariCP pool max |
5 (dedicated pool, separate from app pool) |
csv.batch.chunk-size |
50 |
csv.batch.skip-limit |
100 |