Chapter 24 · Volume III — The Intelligence Layer

Declarative Scheduled Tasks

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

PropertyTypeRequiredDefaultDescription
descriptionstringyesHuman-readable description
recurrencestringif recurringRecurrence expression
typestringno"recurring"recurring or relative
sourceobjectnoRecords to process
source.resourcestringyesResource name, or "*" for all
source.filterstringno""AEL filter expression for matching records
relativeTostringif type: relativeDate field name for relative scheduling
offsetstringif type: relativeDuration offset: -90 days, +30 days
actionstringyesAction type (see §24.5)
targetstringif action: createResource to create records in
inputobjectif applicable{}Field values for create/update actions
transitionstringif action: transitionTransition name
skipIfstringno""AEL expression — skip this record if true
notificationobjectnoInline notification for this schedule
batchedbooleannofalseAggregate source records into one notification
enabledbooleannotrueCan be disabled at runtime

24.5 Action Types

ActionDescriptionRequired Properties
createCreate a new record for each matching source recordtarget, input
updateUpdate fields on each matching source recordinput
transitionExecute a state machine transition on each matching recordtransition
notifySend a notification for each matching record (or batched)channels, recipients, template
archiveExport matching records to external storage and optionally deletedestination, format
deletePermanently delete matching records
reportGenerate a declared report (§26)report
workflowStart a workflow instance (§25)workflow

Input Variables

The input object supports special variables:

VariableDescription
$record.fieldField value from the current source record
$nextOccurrenceNext scheduled date for the recurring task
$currentQuarterCurrent calendar quarter (Q1, Q2, Q3, Q4)
$currentYearCurrent calendar year
$nowCurrent 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

PropertyTypeRequiredDescription
daysUntilintegeryesDays until the relativeTo date (0 = on the date)
actionstringyesAction to perform at this tier
tagstringnoDedup identifier — prevents re-firing for the same record + tier
All other action-specific propertiesSame 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:

  1. Check all enabled schedules whose next_run_at <= now()
  2. 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. Update next_run_at for recurring schedules
  3. 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

ColumnTypeDescription
iduuidLog entry identifier
scheduleNamestringSchedule definition name
startedAtdatetimeWhen execution began
completedAtdatetimeWhen execution finished
statusstringsuccess, partial_failure, failure
recordsProcessedintegerTotal records matched by source filter
recordsSucceededintegerRecords where the action succeeded
recordsFailedintegerRecords where the action failed
recordsSkippedintegerRecords skipped by skipIf
errorsjsonArray of per-record error details
triggeredBystringrecurrence, 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