24.1 Overview
Scheduled tasks add the dimension of time to the domain model. Inspections every 90 days, certification expiry warnings at 90/30/7/0 days, audit retention cleanup monthly, compliance reports weekly — all declared in YAML, executed by the runtime, logged in the execution history.
Scheduled tasks replace cron jobs, cloud function triggers, database agent jobs, and manual checklists. They are visible in the domain model, queryable through the API, and auditable through the execution log.
Prerequisites
Scheduled tasks build on the History (§21) and Notification (§23) systems. Every scheduled action produces history events on affected resources. Schedule-triggered notifications use the notification engine for delivery.
Activation
Scheduled tasks are a domain-level declaration — no feature flag is needed:
domain:
name: operations
version: "1.0.0"
schedules:
# tasks declared here
24.2 Schedule Types
Recurring Schedules
Fixed-interval or calendar-based repetition:
recurrence: every 90 days
recurrence: daily at 06:00
recurrence: weekly on Monday at 08:00
recurrence: monthly on 1st at 02:00
recurrence: monthly on last at 23:00
recurrence: quarterly on 1st at 06:00
recurrence: twice yearly on Jan 15, Jul 15
recurrence: yearly on Mar 31
recurrence: every 4 hours
recurrence: every 15 minutes
Recurrence strings are parsed into a next-execution calculator. The runtime maintains a next_run_at timestamp for each schedule and evaluates on a heartbeat loop (default interval: 60 seconds, configurable).
Relative Schedules
Triggered relative to a date field on a record:
type: relative
source:
resource: Award
filter: "status == 'active'"
relativeTo: endDate
offset: -90 days
Relative schedules scan the source resource on each evaluation cycle and fire for any record where now() has crossed the relativeTo + offset threshold. A deduplication mechanism prevents re-firing: each execution records a (schedule_name, record_id, tier) tuple in the log.
24.3 Declaration
Recurring Record Creation
schedules:
quarterlyInspections:
description: "Create inspection records for critical assets every 90 days"
recurrence: every 90 days
source:
resource: Asset
filter: "status == 'operational' && criticality == 'critical'"
action: create
target: Inspection
input:
assetId: $record.id
assetTag: $record.assetTag
inspectionType: routine
scheduledDate: $nextOccurrence
skipIf: >
exists(Inspection, assetId == $record.id
&& status != 'approved' && status != 'cancelled')
notification:
channels: [email]
recipients:
roles: [inspector]
template:
subject: "Inspection scheduled — {assetTag}"
body: "Routine inspection for {assetTag} scheduled for {scheduledDate}."
Batch State Transitions
schedules:
expireTemporaryMocs:
description: "Expire temporary MOCs that have passed their expiry date"
recurrence: daily at 06:00
source:
resource: ManagementOfChange
filter: "status == 'temporary_approved' && temporaryExpiry < now()"
action: transition
transition: revert
Batch Field Updates
schedules:
flagOverdueInspections:
description: "Flag assets whose inspections are overdue"
recurrence: daily at 07:00
source:
resource: Asset
filter: "status == 'operational' && nextInspectionDue < now() && isOverdue == false"
action: update
input:
isOverdue: true
Notification-Only Tasks
schedules:
effortCertificationReminders:
description: "Remind PIs to certify effort at end of each semester"
recurrence: twice yearly on Jan 15, Jul 15
source:
resource: EffortCommitment
filter: "status == 'committed' || status == 'adjusted'"
action: notify
recipients:
field: personnelId
channels: [email]
template:
subject: "Effort certification due — {period}"
body: |
Please certify your effort allocation for **{period}**.
Current commitments:
{#each records}
- Award {awardNumber}: {committedPercent}% committed
{/each}
batched: true
Data Retention Cleanup
schedules:
auditRetentionCleanup:
description: "Archive audit records older than retention period"
recurrence: monthly on 1st at 02:00
source:
resource: "*"
table: "_audit"
filter: "daysOld(created_at) > retentionDays"
action: archive
destination: "${ARCHIVE_PATH}/audit/{year}/{month}/"
format: csv
deleteAfterArchive: true
24.4 Schedule Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | yes | — | Human-readable description |
recurrence | string | if recurring | — | Recurrence expression |
type | string | no | "recurring" | recurring or relative |
source | object | no | — | Records to process |
source.resource | string | yes | — | Resource name, or "*" for all |
source.filter | string | no | "" | AEL filter expression for matching records |
relativeTo | string | if type: relative | — | Date field name for relative scheduling |
offset | string | if type: relative | — | Duration offset: -90 days, +30 days |
action | string | yes | — | Action type (see §24.5) |
target | string | if action: create | — | Resource to create records in |
input | object | if applicable | {} | Field values for create/update actions |
transition | string | if action: transition | — | Transition name |
skipIf | string | no | "" | AEL expression — skip this record if true |
notification | object | no | — | Inline notification for this schedule |
batched | boolean | no | false | Aggregate source records into one notification |
enabled | boolean | no | true | Can be disabled at runtime |
24.5 Action Types
| Action | Description | Required Properties |
|---|---|---|
create | Create a new record for each matching source record | target, input |
update | Update fields on each matching source record | input |
transition | Execute a state machine transition on each matching record | transition |
notify | Send a notification for each matching record (or batched) | channels, recipients, template |
archive | Export matching records to external storage and optionally delete | destination, format |
delete | Permanently delete matching records | — |
report | Generate a declared report (§26) | report |
workflow | Start a workflow instance (§25) | workflow |
Input Variables
The input object supports special variables:
| Variable | Description |
|---|---|
$record.field | Field value from the current source record |
$nextOccurrence | Next scheduled date for the recurring task |
$currentQuarter | Current calendar quarter (Q1, Q2, Q3, Q4) |
$currentYear | Current calendar year |
$now | Current timestamp |
skipIf
The skipIf expression is evaluated per source record before the action executes. If it evaluates to true, the record is skipped. This prevents duplicate actions:
skipIf: >
exists(Inspection, assetId == $record.id
&& status != 'approved' && status != 'cancelled')
This skips creating a new inspection if an open inspection already exists for the asset.
24.6 Tiered Relative Schedules
Tiered schedules fire at multiple time offsets relative to a date field, with different actions at each tier:
schedules:
certificationExpiryWarnings:
description: "Tiered warnings as certifications approach expiry"
type: relative
source:
resource: Certification
filter: "status == 'active'"
relativeTo: expirationDate
tiers:
- daysUntil: 90
action: notify
tag: 90day
channels: [email]
recipients:
field: personnelId
template:
subject: "Certification expiring in 90 days — {certificationName}"
body: "{personnelName}'s {certificationName} expires on {expirationDate}."
- daysUntil: 30
action: notify
tag: 30day
channels: [email, slack]
recipients:
field: personnelId
roles: [hrManager]
template:
subject: "Certification expiring in 30 days — {certificationName}"
body: "REMINDER: {certificationName} expires on {expirationDate}."
- daysUntil: 7
action: notify
tag: 7day
channels: [email, slack, sms]
recipients:
field: personnelId
roles: [hrManager, operationsManager]
template:
subject: "URGENT — Certification expires in 7 days"
body: "Immediate action required."
- daysUntil: 0
action: transition
transition: expire
notification:
channels: [email, sms]
recipients:
field: personnelId
roles: [hrManager]
template:
subject: "EXPIRED — {certificationName}"
body: "{certificationName} has expired."
Tier Properties
| Property | Type | Required | Description |
|---|---|---|---|
daysUntil | integer | yes | Days until the relativeTo date (0 = on the date) |
action | string | yes | Action to perform at this tier |
tag | string | no | Dedup identifier — prevents re-firing for the same record + tier |
| All other action-specific properties | — | — | Same as §24.5 |
Deduplication
Each tier fires once per record. The runtime tracks (schedule_name, record_id, tag) tuples in the execution log. Once a tier has fired for a specific record, it is not re-fired on subsequent evaluations.
24.7 Execution Model
Heartbeat Loop
The scheduler runs on the server’s heartbeat loop (default: every 60 seconds). On each heartbeat:
- Check all enabled schedules whose
next_run_at <= now() - For each due schedule:
a. Query source records matching the filter
b. For each record, evaluate
skipIf— skip if true c. Execute the action (create, update, transition, notify, archive, delete) d. Log the execution result e. Updatenext_run_atfor recurring schedules - For relative schedules:
a. Query source records matching the filter
b. Calculate
daysUntil(relativeTo)for each record c. Fire any tiers that have crossed their threshold d. Check dedup log to prevent re-firing
Execution as System Actor
Scheduled actions execute as the system actor (no authenticated user). The actor identity in history events and audit records is system with an actor name of the schedule name:
{
"actorId": "system",
"actorName": "quarterlyInspections"
}
Error Handling
If an action fails for a specific record (validation error, transition error, permission error), the failure is logged and the schedule continues processing remaining records. A single record failure does not halt the entire schedule run.
24.8 Schedule API
List Schedules
GET /api/v1/{domain}/schedules
Returns all schedule definitions with status, next run time, and last run result.
Get Schedule
GET /api/v1/{domain}/schedules/{name}
Returns one schedule definition with full execution statistics.
Execution Log
GET /api/v1/{domain}/schedules/{name}/log
?since=2026-01-01&status=success
Returns the execution history for a schedule: when it ran, how many records were processed, how many succeeded, how many failed, duration.
Manual Trigger
POST /api/v1/{domain}/schedules/{name}/run
Execute the schedule immediately, outside its normal recurrence. Admin-only. Returns the execution result. Useful for testing and one-off runs.
Enable / Disable
POST /api/v1/{domain}/schedules/{name}/enable
POST /api/v1/{domain}/schedules/{name}/disable
Toggle a schedule at runtime. The state is persisted and survives restarts.
24.9 Execution Log Schema
| Column | Type | Description |
|---|---|---|
id | uuid | Log entry identifier |
scheduleName | string | Schedule definition name |
startedAt | datetime | When execution began |
completedAt | datetime | When execution finished |
status | string | success, partial_failure, failure |
recordsProcessed | integer | Total records matched by source filter |
recordsSucceeded | integer | Records where the action succeeded |
recordsFailed | integer | Records where the action failed |
recordsSkipped | integer | Records skipped by skipIf |
errors | json | Array of per-record error details |
triggeredBy | string | recurrence, relative, manual |
24.10 Integration Points
Schedules + History
Every scheduled action produces history events on affected resources. A quarterly inspection schedule creates Inspection records, each producing a created history event. The schedule execution log is separate from but complementary to resource history — together they answer “what happened to this asset?” (resource history) and “when did the system last check it?” (schedule log).
Schedules + Notifications
Schedules can include inline notification definitions (notification: on the schedule) or fire actions that produce history events, which then trigger standalone notification definitions (§23). Inline notifications are simpler for schedule-specific messages. Standalone notifications are reusable across schedules and manual triggers.
Schedules + Workflows
A schedule can start a workflow instance via action: workflow. The workflow definition (§25) is referenced by name and receives the source record as input. This enables time-triggered multi-step processes: “on the first of each month, start the monthly reporting workflow.”
Schedules + Rules
Scheduled updates and transitions are subject to the rules engine (§22). If a scheduled transition triggers a rule violation (e.g., transitioning an asset to operational status without a recent inspection), the rule fires and the action fails for that record. The failure is logged in the schedule execution log.
End of Chapter 24.
Next: Chapter 25 — Declarative Workflows