Feature Specification: Scheduled Work Updates
Feature ID: scheduled-updates
Status: Retrospective
Created: 2026-05-01
Last updated: 2026-05-01
Owner: Ever Works Team
1. Overview
A work can be configured to re-run its generation pipeline on a
recurring cadence (hourly through monthly, plus the every-N-hours variants).
The platform handles fan-out via a Trigger.dev cron task, atomic per-schedule
claiming via a single SQL UPDATE, drift correction via a scheduledFor
anchor, retry-on-failure with exponential pause, and zombie recovery.
2. User Scenarios
2.1 Primary scenarios
- Given I enable "weekly" scheduled updates on my work, when a week passes since my last generation, then the platform automatically runs a fresh generation and I see the new result on the work page.
- Given my schedule is
active, when I click "Run Now", then a generation starts immediately without resetting my upcoming weekly slot. - Given my schedule failed three times in a row, when the
maxFailureBeforePausethreshold is hit, then the schedule is auto-paused and I get a notification. - Given my schedule is
paused, when I re-enable it, thennextRunAtis recomputed and the schedule resumes on the next cron tick.
2.2 Edge cases & failures
- Given the dispatcher tick is slow and overlaps with the next tick,
when both tries to claim the same due schedule, then exactly
one succeeds and the other records
outcome: skipped. - Given a generation crashes without finalising, when the next
dispatcher tick runs, then the schedule is detected as zombie
(over
getScheduleStuckTimeoutMinutesold inGENERATING) and flipped toERRORso it becomes eligible again. - Given my plan doesn't include the cadence I want, when I select
billingMode: usage, then the platform allows the cadence on pay-per-use billing. - Given my work was deleted between the dispatcher's "find due"
query and the actual dispatch, when the dispatch tries to run,
then the outcome is
skippedwith reasonwork_not_found.
3. Functional Requirements
- FR-1 The system MUST expose
GET / PUT / DELETE / POST runendpoints under/api/works/:id/schedulefor managing a work's schedule. - FR-2 The system MUST accept all seven cadence values:
hourly,every_3_hours,every_8_hours,every_12_hours,daily,weekly,monthly. - FR-3 The system MUST run a Trigger.dev cron task at
*/N * * * *whereN = config.subscriptions.getDispatchIntervalMinutes(). - FR-4 The dispatcher MUST claim each due schedule using a single
atomic SQL
UPDATE … SET nextRunAt = NULL WHERE id = X AND status = ACTIVE AND nextRunAt IS NOT NULL. The first updater wins; the loser seesaffected = 0and reportsoutcome: skipped. - FR-5 The dispatcher MUST preserve the original
nextRunAtintoscheduledForat claim time and use it as the anchor for computing the nextnextRunAtafter completion (drift correction). - FR-6 The dispatcher MUST detect zombie schedules (status
GENERATINGolder thangetScheduleStuckTimeoutMinutes, default 60 min) and flip them toERRORbefore claiming new work. - FR-7 The dispatcher MUST process schedules sequentially (not in
parallel) within a single tick, capped by
config.subscriptions.getMaxBatch(). - FR-8 The dispatcher MUST return a
WorkScheduleDispatchSummarywithdueCount,dispatched,skipped,failedcounts and a per- entry breakdown including outcome and reason. - FR-9 On failure, the schedule's
failureCountMUST increment; when it reachesmaxFailureBeforePause, the schedule MUST be auto-paused and a notification sent. - FR-10 "Run Now" requests that fire before the next scheduled slot
MUST preserve the existing
nextRunAt(no schedule drift from manual runs). - FR-11 The system MUST support per-schedule
providerOverridesforai,search,screenshot,contentExtractor,pipelineplugin ids; each override MUST reference an installed plugin. - FR-12 The system MUST track per-schedule
billingMode(subscriptionorusage);usagemode MUST allow cadences not included in the user's plan.
4. Non-Functional Requirements
- Performance: dispatcher tick completes within the cron interval
(
getDispatchIntervalMinutes()) at typical load; a single tick handlesgetMaxBatch()works. - Reliability: at-most-once dispatch per
nextRunAtslot, even with overlapping ticks; multi-worker safe via the SQL CAS pattern. - Security & privacy: schedule endpoints require work edit rights; all cron tasks run server-side with elevated DB access.
- Observability: per-tick summary returned to Trigger.dev (visible in its dashboard); auto-pause emits a notification.
- Compatibility: cadence enum is additive; new cadences can be added without breaking existing schedules.
5. Key Entities & Domain Concepts
| Entity / concept | Description |
|---|---|
WorkSchedule | Per-work schedule row: cadence, status, nextRunAt, scheduledFor, billingMode |
WorkScheduleStatus | Enum: disabled / active / paused / canceled |
WorkScheduleCadence | Enum: 7 values from hourly to monthly |
WorkScheduleBillingMode | Enum: subscription / usage |
WorkScheduleDispatchSummary | Trigger.dev task return value with counts + per-entry outcomes |
scheduledFor (anchor) | The original nextRunAt preserved at claim time; drift-correction reference |
6. Out of Scope
- Custom cron expressions (only the seven canonical cadences are exposed).
- Schedule chaining (schedule A finishes → trigger schedule B).
- Per-step scheduling (the schedule fans out to a full generation, not individual pipeline steps).
7. Acceptance Criteria
- All seven cadence values are accepted by the API.
- Concurrent dispatchers cannot both claim the same schedule (CAS integration test).
- Drift is bounded — a schedule that fires 90 s late stays anchored on its original slot.
- Zombie recovery flips stuck schedules to ERROR after the configured timeout.
- Auto-pause kicks in after
maxFailureBeforePauseconsecutive failures. - "Run Now" before the next slot preserves the slot.
- Provider overrides validate plugin ids against the registry.
-
usagebilling mode allows cadences outside plan limits.
8. Open Questions
None on develop.
9. Constitution Gates
- I: N/A.
- II: provider overrides are capability-keyed, not plugin-internal.
- III: schedules don't replicate user content into the database; they reference the user's repo.
- IV: dispatcher runs as a Trigger.dev
schedules.task— the canonical example. - V: cadence enum extensions ship as additive migrations; the
introduction of
scheduledForwas a forward migration with defaultnull. - VI: covered by repository spec, dispatcher spec, scheduling
service spec, integration tests (
schedule.repository.spec.ts,work-schedule-dispatcher.spec.ts,work-schedule.service.spec.ts). - VII: schedule rows do not store secrets.
- VIII: N/A.
- IX: this spec describes user-observable behaviour.
- X: cadence enum and status enum are additive.
10. References
- User-facing doc:
../../../features/scheduled-updates.md - Internal architecture:
../../../agent-services/work-schedule-dispatcher.md - Implementation:
packages/agent/src/services/work-schedule.service.tspackages/agent/src/services/work-schedule-dispatcher.service.tspackages/agent/src/database/repositories/work-schedule.repository.tspackages/tasks/src/tasks/trigger/work-schedule-dispatcher.task.ts
- Related:
works-config/spec.md(schedule cadence is mirrored to.works/works.yml)