Skip to main content
Version: v5.6.1

Scheduled activities

Included from Basic tier

A scheduled activity is a standing schedule that creates work on the clock: a row on a project holding a workflow code and a firing schedule — a repeating cron expression or a one-off instant — that the server turns into an ordinary workflow run at each due time. Two row types share the resource: scheduled workflows, fully writable, and scheduled changes, legacy rows that remain readable (listed and shown) but whose writes are refused by name — the refusal states that scheduled change writes are not implemented on this server and only scheduled workflows can be created or updated. The runs a schedule fires are not a separate lane: each firing crosses the same create door a user's run-create crosses, so licensing, maintenance gating, approval policies and the full run-create validation all apply per firing.

Its neighbours: the trigger engine (opschain docs triggers) is event-driven automation and also owns the tick this component rides; scheduled activities are the clock-driven lane beside it. The hands-on runbook is opschain learn 5.2.

The schedule grammar​

Exactly one of two shapes, enforced at write time as an XOR pair:

  • Repeating — repeat: true with a cron_schedule: a 5-field cron expression (minute hour day-of-month month day-of-week) supporting *, lists, ranges and steps. Day-of-month and day-of-week follow the standard cron rule: when both are restricted the date matches either; when one is *, the other alone decides. 7 in day-of-week is Sunday, same as 0. Setting run_at beside a cron is a named refusal.
  • One-shot — run_at, an RFC 3339 instant, with no cron. A past run_at fires on the next sweep.

All schedule arithmetic is UTC: occurrences are computed in UTC and timestamps are stored UTC, so a cron expression means UTC wall-clock time. A cron that does not parse names the offending field; one that never matches a future time (e.g. 0 0 31 2 *) is refused at write — Cron schedule never matches a future time.

Bounding fields shape when the schedule stops: end_at (an instant after which it never fires again) and maximum_run_count (at least 1; the schedule disables once it has fired that many times, whatever each run's eventual outcome). enabled pauses and resumes the whole row without losing its definition.

What a write validates​

Creates land on a project (the workflow runs there); updates merge the supplied fields over the stored row and re-run the full create validation, recomputing the next due time whenever a schedule-shaping field (cron_schedule, run_at, repeat, enabled) moves:

  • the workflow must exist on the project and not be archived;
  • a pinned version must resolve, and the stored typed inputs object is dry-run at schedule time against that version's declarations exactly as run creation would validate it — unknown names, type and enum mismatches and unsatisfied required inputs are named refusals before anything is stored. An unpinned schedule (no version, or latest) resolves latest-at-firing, so the static dry run is skipped and every firing re-validates instead;
  • metadata, property_overrides and notify must each be a JSON object, and metadata carrying the reserved top-level opschain key is refused (the engine stamps its own keys there);
  • owned_by, where set, must name a known group;
  • run_as, where set, needs the acting principal's act_as trust grant covering the node — the same write-time guardrail trigger run_as carries — and the named identity must resolve.

run_as and owned_by are written through the typed v2 service (and the surfaces built on it); the v1 payload does not accept them. Secret-typed inputs accept vault references only, and every read of the schedule serves them masked (******) — an undecodable inputs document masks whole, fail-closed.

Firing semantics​

The trigger engine's 2-second tick sweeps due rows — enabled with a next_run_at at or before now, on a live node. Concurrent engine replicas each claim a disjoint set of due rows inside one transaction, and the row's schedule state moves under the same claim, so a firing happens exactly once per due time across an HA deployment. Rows adopted from a pre-migration OpsChain server that still owns their scheduling (no native enabled/next-run state) are left to that scheduler — the two engines never double-fire one schedule.

Each claimed row settles in order:

  1. Lifecycle gates — a due time past end_at, or a run_count already at maximum_run_count, disables the schedule instead of firing.
  2. The overlap guard — unless allow_parallel_execution is set, a still-unfinished run of this schedule defers the firing: a repeating schedule moves to its next cron occurrence (the due fire is skipped, never queued behind the live run), a one-shot retries each minute until the live run finishes. The server default is no parallel execution.
  3. The create — the run is created through the ordinary gateway as the executing identity: the run_as identity when one is set, the schedule's owner otherwise, re-resolved and RBAC-checked at firing time (read and update on the project — authorisation applies to firings exactly as to users). The licence work-stop, maintenance windows, approval-policy matching and typed-input validation all run per firing. The created run is stamped with its schedule (scheduled_activity_id, and opschain.scheduled_activity_id in its metadata), the run_count moves, and the schedule advances — a repeating row to its next occurrence (disabling when that occurrence would pass end_at, when the count is reached, or when the stored cron no longer yields one), a one-shot to disabled.

A failed firing is loud, never fatal: a create the gates refuse — an expired licence, a covering maintenance window whose allow lists do not admit the workflow or the executing username, a workflow that has gone missing, an input a firing-time re-validation rejects, an owner or run_as identity that no longer resolves — records a warn:scheduled_activities:error event on the schedule's node, then disables a one-shot or advances a repeating schedule past the bad firing. A scheduled firing is not parked the way a trigger firing defers under maintenance (opschain docs maintenance-windows) — the error event and the advance are its whole story. A schedule whose owner has left is repointed with opschain admin users reassign-automations (opschain docs cli/admin-users-reassign-automations).

Missed occurrences coalesce: a schedule that came due while no server was running fires once at the next sweep, then advances past now — downtime never queues a backlog of catch-up runs.

A funnel-held firing behaves like any held run: where an approval policy matches the created run, it is created and held pending_approval — schedule lineage is matched by the governance funnel exactly as user lineage is (opschain docs change-governance).

Surfaces​

SurfaceDoes
GET /api/scheduled_activitiesThe estate listing (both row types); denied rows never appear.
POST /api/scheduled_activitiesCreate a scheduled workflow (201); node-scoped variants exist under the node path.
GET /api/scheduled_activities/{id}One row's full attribute document.
PATCH /api/scheduled_activities/{id}Partial update; the merged state re-validates in full.
DELETE /api/scheduled_activities/{id}Hard delete — future firings never happen, already-fired runs stay ordinary run history.

The CLI family is opschain scheduled-activities list|show|create|update|delete (opschain docs cli/scheduled-activities and its subpages); create and update load a flat YAML definition whose keys mirror the writable attributes verbatim.

Authorisation is node-scoped, 404-on-deny: reads ride read on the owning node's scheduled_activities rule path (denied rows simply never list), creates need read on the parent node plus update on that path, updates read plus update, deletes read plus delete.

No config.yaml key tunes this component: the sweep cadence is the trigger engine's built-in 2-second tick, and the one-shot defer interval (one minute) is built in. Schedule creation carries no feature or pack entitlement gate of its own — the licence speaks at firing time, through the run-create door, where an expired licence's work-stop refuses the create loudly.

Events vocabulary​

  • warn:scheduled_activities:error — a firing could not start; the event names the workflow, the error, and the executing/owning identities where set. The schedule's visible failure signal.
  • api:scheduled_workflows:create / update / destroy — the write audit trail (one of the API write families on opschain docs events).
  • The runs a schedule fires land the ordinary run lifecycle events, each carrying the schedule's id in its metadata stamp.
  • opschain docs cli/scheduled-activities — the command family, verb by verb.
  • opschain docs triggers — the event-driven lane, and the engine tick this one rides.
  • opschain docs maintenance-windows — what a covering window refuses or admits at the firing's create door.
  • opschain docs change-governance — approval policies over schedule lineage.
  • opschain docs events — where the vocabulary above lands.

The API surface is ScheduledActivityService (see opschain docs api-v2): GetScheduledActivity, ListScheduledActivities, CreateScheduledActivity, UpdateScheduledActivity, DeleteScheduledActivity — reads cover both row types, writes are scheduled-workflow only, with the same named refusal as v1.