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: truewith acron_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.7in day-of-week is Sunday, same as0. Settingrun_atbeside a cron is a named refusal. - One-shot —
run_at, an RFC 3339 instant, with no cron. A pastrun_atfires 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
versionmust resolve, and the stored typedinputsobject 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, orlatest) resolves latest-at-firing, so the static dry run is skipped and every firing re-validates instead; metadata,property_overridesandnotifymust each be a JSON object, and metadata carrying the reserved top-levelopschainkey 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'sact_astrust grant covering the node — the same write-time guardrail triggerrun_ascarries — 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:
- Lifecycle gates — a due time past
end_at, or arun_countalready atmaximum_run_count, disables the schedule instead of firing. - The overlap guard — unless
allow_parallel_executionis 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. - The create — the run is created through the ordinary gateway as
the executing identity: the
run_asidentity 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, andopschain.scheduled_activity_idin its metadata), therun_countmoves, and the schedule advances — a repeating row to its next occurrence (disabling when that occurrence would passend_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
| Surface | Does |
|---|---|
GET /api/scheduled_activities | The estate listing (both row types); denied rows never appear. |
POST /api/scheduled_activities | Create 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 onopschain docs events).- The runs a schedule fires land the ordinary run lifecycle events, each carrying the schedule's id in its metadata stamp.
Related pages
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.