Appearance
Schedule Domain
Core Entity: ScheduledJob
ScheduledJob tracks a scheduling request from creation through its terminal state. It extends BaseEntity<ScheduledJobProps>.
Key properties:
| Property | Type | Notes |
|---|---|---|
id | string (UUIDv7) | Generated on creation |
topic | string | Routing key — max 3 dot-separated lowercase segments |
kind | 'one_shot' | 'cron' | 'interval' | Determines which BullMQ option is used |
status | 'pending' | 'active' | 'completed' | 'failed' | 'cancelled' | State machine (see invariants) |
runAt | Date | null | UTC instant — only set for one_shot |
cronPattern | string | null | 5-field cron — only set for cron |
intervalMs | number | null | ms — only set for interval |
timezone | string | IANA timezone (e.g. "America/Mexico_City") |
payload | Record<string, unknown> | Opaque JSON delivered to listeners |
metadata.ownerId | string | null | UUIDv7 — audit/filter only |
metadata.tenantId | string | null | UUIDv7 — audit/filter only |
metadata.correlationId | string | null | UUIDv7 — tracing |
metadata.clientRequestId | string | null | UUIDv7 — idempotency key |
retryPolicy | object | null | Per-job override; defaults apply when null |
attempts | number | How many times the worker has tried |
maxAttempts | number | 5 by default |
lastError | string | null | Set on failure |
firedAt | Date | null | UTC instant of first successful worker entry |
Status machine:
pending → active → completed
→ failed (retrying until maxAttempts; then terminal)
→ cancelled
active → cancelled (prevents further retries)completed, failed, and cancelled are terminal — no further transitions.
Value Objects
| File | What it validates |
|---|---|
ScheduleTopicVo | Regex ^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*){0,2}$ — 1 to 3 lowercase segments |
ScheduleTimezoneVo | Valid IANA string via Luxon IANAZone.isValidZone |
ScheduleSpecVo | Discriminated union: OneShot (requires runAt > now) | Cron (valid 5-field expression) | Interval (everyMs ≥ 1000) |
ScheduledJobIdVo | Branded UUIDv7 wrapper |
ScheduleKind | Enum: one_shot | cron | interval |
ScheduleStatus | Enum: pending | active | completed | failed | cancelled |
IScheduleService — Public API
Declared in domain/services/schedule.service.interface.ts. Injected via ScheduleServiceKey symbol.
ts
export interface IScheduleService {
scheduleAt(spec: IScheduleOneShotSpec): Promise<IScheduledJob>;
scheduleRepeat(spec: IScheduleRepeatSpec): Promise<IScheduledJob>;
cancel(id: string): Promise<void>;
reschedule(id: string, newSpec: IScheduleOneShotSpec | IScheduleRepeatSpec): Promise<IScheduledJob>;
list(filter: IScheduleListFilter): Promise<IScheduleListResult>;
getById(id: string): Promise<IScheduledJob | null>;
}Metadata fields are for audit and routing only — not access control. Callers must authorize the request before calling IScheduleService. See Authorization Boundary.
IScheduleArrivedEvent — Event Contract
Declared in domain/events/schedule-arrived.event.interface.ts.
ts
export interface IScheduleArrivedEvent {
scheduledJobId: string;
topic: string;
userPayload: Record<string, unknown>;
metadata: {
ownerId: string | null;
tenantId: string | null;
correlationId: string | null;
clientRequestId: string | null;
};
timezone: string; // IANA string echoed from original spec
originalScheduledAt: Date; // UTC
firedAt: Date; // UTC — worker entry instant
attempt: number; // 1-based
maxAttempts: number;
}Two event names are always emitted per fire:
| Event name | Pattern |
|---|---|
schedule.arrived | Uniform — all jobs |
schedule.<topic>.arrived | Per-topic alias (e.g. schedule.notifications.task-reminder.arrived) |
Error Codes
| Code | Capability | Meaning |
|---|---|---|
SCHEDULE_MOMENT_IN_PAST | scheduleAt | runAt ≤ current UTC |
SCHEDULE_TIMEZONE_INVALID | both | timezone is not a valid IANA string |
SCHEDULE_TOPIC_INVALID | both | topic fails regex or exceeds 3 segments |
SCHEDULE_CRON_INVALID | scheduleRepeat | pattern is not valid 5-field cron |
SCHEDULE_INTERVAL_TOO_SHORT | scheduleRepeat | everyMs < 1000 |
SCHEDULE_CLIENT_REQUEST_ID_IN_USE | both | clientRequestId already exists in any status |
SCHEDULE_JOB_NOT_FOUND | reschedule | no job with given id |
SCHEDULE_JOB_NOT_CANCELLABLE | reschedule | job exists but status is not 'pending' |
SCHEDULE_JOB_ALREADY_CANCELLED | cancel | informational only — not thrown, logged at INFO |
SCHEDULE_RETRY_POLICY_INVALID | both | retry values outside allowed bounds |
SCHEDULE_ENQUEUE_FAILURE | both | BullMQ enqueue failed after Postgres write |