Scheduling and workflows

Table of Contents

A schedule determines when work starts; a workflow run executes work that lasts longer than one action. The connection is one-directional: the job's action publishes a message, the run's owner creates the row the run acts on and starts the run, and the scheduler's part ends at the reply. This page defines the boundary from the scheduler's side: what the trigger and the start message carry, what the job must not wait for, and what the scheduler must not read. The design of the run belongs to the workflow cluster, and this page assumes only that a run exists.

1. Detail

1.1. The job starts the run; it does not execute it

Each action type has a defined reach: a database statement, a queue message, or a published message. None of them executes work that outlives one firing. When the work must outlive it, the job's action publishes a message rather than executing anything, and which message depends on whether the run needs a domain row first.

A run with no such row is started directly on the workflow start subject, and the engine, as process manager, creates the run and dispatches the steps the run's definition declares. A run that acts on a domain row is started through the service that owns the row: the action calls that service's trigger, the service creates the row, and the service publishes the start. Report execution takes the second route, because a report instance must exist before its run does. Either way the run holds the steps, records each step's result, and ends when the last declared step completes.

The job does not wait for that end and does not read the run's state. The publish is fire-and-forget: it returns when the message is sent, and the run continues independently. The same rule applies to every action the scheduler performs, because the scheduler's responsibility ends when the action returns.

1.2. The trigger and the start message

A job with a published action names a subject and a body. A trigger message carries what the owner needs to create its row, which for a report is the definition, the tenant and the firing's identity.

The scheduler sends the trigger as itself, by Client Credentials, because a firing has no caller. The trigger therefore carries no tenant identity. When the run must act as its tenant and party, the owner uses Scheduled Work Acting as a Tenant.

The start message is published on the workflow start subject, by the owner rather than by the scheduler. It carries the workflow type, the tenant, the request payload, the correlation identifier, and the instance identifier of the run.

scheduled_workflow_start.png

Figure 1: The start message a caller publishes: the run's identity is named in the message, so re-publishing one start re-opens the same run.

Source: scheduled_workflow_start.puml. Regenerate with plantuml -tpng doc/knowledge/architecture/scheduled_workflow_start.puml.

The run's instance identifier is named in the start message, so re-publishing one start opens the same run rather than a second one. The engine offers that idempotency and the caller supplies the identifier. A caller that routes through a domain service passes the firing's identity to that service instead, and the service records it against the row it creates; report execution does this, and stores the firing on the report instance as its trigger run. What neither route may do is start two runs for one occurrence.

The interaction has one exchange. The scheduler publishes the trigger and records the reply; everything after the start belongs to the run.

scheduled_workflow_start_sequence.png

Figure 2: A firing that starts a run: the scheduler publishes the trigger and records the reply, the run's owner creates its row and publishes the start, and the run then proceeds without the scheduler.

Source: scheduled_workflow_start_sequence.puml. Regenerate with plantuml -tpng doc/knowledge/architecture/scheduled_workflow_start_sequence.puml.

1.3. The three obligations at the boundary

Each side owns part of the boundary. Three obligations fall to the scheduler, and each follows from the scheduler's own design.

  • Name the run's identity, or pass the firing's on. A caller that publishes the start supplies the run's instance identifier, so a repeated publish opens the same run. A caller that routes through a domain service passes the firing's identity to that service, which records it against the row it creates and publishes the start itself.
  • Publish and return. The job never waits for the run and never reads its state. A job that waited would hold the scheduler's loop for the duration of the run.
  • Do not hold the run's definition. The steps, their subjects and their failure policy belong to the workflow definition. The job holds the start and no part of the run's shape, so a change to the definition reaches the next run without changing any schedule.

1.4. The scheduler and the run are independent after the start

The schedule can fire again while an earlier run is in flight. Each firing starts a separate run, so a slow run does not delay the next firing and a failed run does not suppress it. A cron expression defines when work starts and says nothing about how long that work takes.

Once a run exists, the schedule is not how a caller follows it. A caller follows a run through the progress read, which reports the run's state and the state of each step, so a screen renders the work without subscribing to the schedule that started it. Workflows defines that read and the states it reports.

1.5. A step deadline is not a scheduled job

A step that must expire holds a time budget. A cron expression defines a recurring set of instants, not one arbitrary instant, so it cannot express a deadline for a single run. The step therefore arms its own timer on the executor that owns its subscription. Recurrence belongs to the scheduler, and expiry belongs to the step that declares it. Waiting, deadlines and cancellation defines where the budget belongs and what its expiry reports.

1.6. Adding a scheduled trigger to a workflow

Three steps connect a new schedule to a run, and each belongs to one owner.

  1. Register the job definition with the cron expression and the action that starts the run.
  2. Point the action at the subject that starts the run: the workflow start subject when the run needs no domain row, or the domain service's trigger when it does.
  3. Leave the steps, their subjects and their failure policy in the workflow definition, where the step protocol defines what each step must implement.

A schedule connected this way has a job definition whose only task is to start a run, and a run that owns everything after the start.

2. See also

  • Scheduler — the structure note that orders this cluster, and where to read this page in it.
  • The Open Group, crontab — the notation a scheduled start recurs on.

Emacs 29.3 (Org mode 9.6.15)