Skip to main content

Scheduling Module

Overview

The Scheduling module in @ever-works/agent manages automated, recurring updates for works. It provides cadence-based scheduling (hourly, daily, weekly, monthly), integrates with the subscription/billing system for plan enforcement, handles failure recovery with configurable retry limits, and implements drift-correction to maintain consistent execution timing.

The scheduler is designed to work with the Trigger.dev background job system -- it manages schedule state and entitlements while the actual execution dispatch happens through the WorkGenerationDispatcher interface.

Module Structure

packages/agent/src/
services/
work-schedule.service.ts # Core scheduling service (~571 lines)
work.module.ts # Registered as part of WorkModule
entities/
work.entity.ts # Schedule fields on Work entity
work-schedule.entity.ts # WorkSchedule entity (1:1 with Work)
types.ts # WorkScheduleCadence, WorkScheduleStatus
subscriptions/
subscription.service.ts # Plan enforcement for scheduling
tasks/
work-generation-dispatcher.ts # Dispatcher interface
work-generation.types.ts # Dispatch payload types

Key Classes and Services

WorkScheduleService

The core service (~571 lines) managing all scheduling operations:

Schedule management:

  • getSchedule(workId) -- retrieve the current schedule configuration and status
  • updateSchedule(work, user, cadence, options) -- enable or update the schedule. Validates entitlements, calculates next run time, and persists the schedule.
  • cancelSchedule(work, user) -- disable scheduling entirely. Clears the cadence, next run time, and resets status.
  • pauseSchedule(work, user, reason?) -- temporarily pause without losing configuration. The cadence is preserved but no runs will be dispatched.

Run lifecycle:

  • markRunDispatched(workId) -- mark that a scheduled run has been sent to the background worker
  • markRunCompleted(workId) -- mark successful completion. Calculates the next run time using drift correction.
  • markRunFailed(workId, error) -- record a failure. Increments the failure counter and potentially auto-pauses the schedule.

Recovery:

  • recoverStuckSchedules() -- find schedules that have been in "dispatched" status for too long and reset them for retry. This handles cases where the background worker crashed or timed out.
  • validateRunEntitlement(work, user) -- check that the user's subscription plan allows the scheduled run to proceed.

Cadence System

Four cadence levels are supported:

CadenceIntervalTypical Use Case
HOURLYEvery 60 minutesHigh-frequency monitoring works
DAILYEvery 24 hoursStandard content works
WEEKLYEvery 7 daysSlower-moving curated lists
MONTHLYEvery 30 daysArchival or low-update works

Drift Correction

The scheduler calculates the next run time based on the intended execution time, not the actual completion time. This prevents gradual time drift:

Intended: 08:00 daily
Actual completion: 08:07
Next run: 08:00 tomorrow (not 08:07 tomorrow)

If the calculated next time is already in the past (e.g., the run took longer than the interval), the scheduler skips forward to the next valid slot.

Failure Handling

The service implements configurable failure limits:

  • maxFailureBeforePause -- configurable per schedule (1-10, default: 3)
  • On each failure, the failure counter increments
  • When the counter reaches the limit, the schedule is automatically paused
  • A 15-minute retry delay is added after each failure before the next attempt
  • Failures are recorded with error messages for diagnostics

Billing Integration

Two billing modes are supported:

ModeBehavior
SUBSCRIPTIONSchedule runs are included in the subscription plan. Cadence options are gated by plan tier.
USAGEPay-per-use billing. Each run is metered and billed separately.

Plan enforcement validates:

  • Allowed cadence levels per subscription tier
  • Maximum number of works with active schedules
  • Per-run entitlement check before dispatch

API Reference

WorkScheduleService

// Schedule management
getSchedule(workId: string): Promise<ScheduleInfo>

updateSchedule(
work: Work,
user: User,
cadence: WorkScheduleCadence,
options?: {
maxFailureBeforePause?: number; // 1-10, default: 3
startAt?: Date; // When to begin (default: now + interval)
}
): Promise<ScheduleInfo>

cancelSchedule(work: Work, user: User): Promise<void>
pauseSchedule(work: Work, user: User, reason?: string): Promise<void>

// Run lifecycle
markRunDispatched(workId: string): Promise<void>
markRunCompleted(workId: string): Promise<void>
markRunFailed(workId: string, error: string): Promise<void>

// Recovery and validation
recoverStuckSchedules(): Promise<number> // Returns count of recovered schedules
validateRunEntitlement(work: Work, user: User): Promise<{
allowed: boolean;
reason?: string;
}>

ScheduleInfo

interface ScheduleInfo {
enabled: boolean;
cadence: WorkScheduleCadence | null;
status: WorkScheduleStatus | null;
nextRunAt: Date | null;
lastRunAt?: Date;
lastError?: string;
failureCount: number;
maxFailureBeforePause: number;
}

WorkScheduleCadence

type WorkScheduleCadence = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY';

WorkScheduleStatus

type WorkScheduleStatus = 'active' | 'paused' | 'dispatched' | 'running' | 'error';

Configuration

Work Entity Schedule Fields

The Work entity carries inline schedule fields for quick-access:

@Column({ type: 'boolean', default: false })
scheduledUpdatesEnabled: boolean;

@Column({ type: 'varchar', nullable: true })
scheduledCadence?: WorkScheduleCadence | null;

@TimestampColumn({ nullable: true })
scheduledNextRunAt?: Date | null;

@Column({ type: 'varchar', nullable: true })
scheduledStatus?: WorkScheduleStatus | null;

WorkSchedule Entity

The WorkSchedule entity (one-to-one with Work) stores extended scheduling metadata:

  • failureCount -- current consecutive failure count
  • maxFailureBeforePause -- threshold for auto-pause
  • lastRunAt / lastError -- most recent run information
  • pausedReason -- reason for pause (if paused)
  • billingMode -- SUBSCRIPTION or USAGE

Dispatch Integration

Scheduled runs are dispatched through the WorkGenerationDispatcher interface:

interface WorkGenerationDispatcher {
dispatchWorkGeneration(payload: WorkGenerationPayload): Promise<string | null>;
}

interface WorkGenerationPayload {
workId: string;
userId: string;
options?: {
aiProviderOverride?: string;
};
}

The dispatcher is injected via the WORK_GENERATION_DISPATCHER Symbol token, decoupling the scheduling logic from the specific background job implementation (Trigger.dev).

Dependencies

DependencyPurpose
@ever-works/agent/databaseWorkRepository, WorkScheduleRepository
@ever-works/agent/subscriptionsPlan enforcement and billing mode resolution
@ever-works/agent/tasksWORK_GENERATION_DISPATCHER for background dispatch
TypeORMEntity persistence and queries

Usage Examples

Enabling a Daily Schedule

import { WorkScheduleService } from '@ever-works/agent/services';

const schedule = await scheduleService.updateSchedule(work, user, 'DAILY', { maxFailureBeforePause: 5 });

console.log(`Next run at: ${schedule.nextRunAt}`);

Handling Run Completion

// Called by the background worker after successful generation
await scheduleService.markRunCompleted(workId);
// Next run time is calculated with drift correction

// On failure
await scheduleService.markRunFailed(workId, 'AI provider rate limit exceeded');
// Auto-pauses after maxFailureBeforePause consecutive failures

Recovering Stuck Schedules

// Run periodically (e.g., every 30 minutes) to recover orphaned runs
const recovered = await scheduleService.recoverStuckSchedules();
console.log(`Recovered ${recovered} stuck schedules`);

Checking Entitlements

const { allowed, reason } = await scheduleService.validateRunEntitlement(work, user);
if (!allowed) {
console.log(`Schedule blocked: ${reason}`);
// e.g., "Plan does not allow HOURLY cadence"
}