5.3 Time and the outside world: schedules and inbound webhooks
triggers
After this you can: run work on a schedule, and let an external system raise a trigger through an inbound webhook.
Before you start — tick each one:
- Lesson 5.2 — Triggers (
opschain learn 5.2)
In the GUI: the Scheduled activity tab on a project, environment or asset page, beside Triggers.
Lesson 5.2 reacted to OpsChain's own events. Two more sources start work in an estate: the clock, through scheduled activities, and the outside world, through inbound webhooks that let a CI server or a git host fire your triggers directly. By the end you will have a nightly schedule with typed inputs and a signed endpoint an external system can post to — with the identity each one executes as understood.
1. Scheduled activities: time-based automation
Scheduled activities fire on the clock — a cron expression (or a
one-shot run_at) that creates a workflow run on every due tick. They
are the same automation discipline one lane over, and the server records
their firings with the same honesty.
The CLI manages the schedule the same way it manages triggers — a strict
flat YAML definition whose keys mirror the API attributes verbatim. The
project is the positional argument, and a workflow's typed inputs
(lesson 4.1) ride along in inputs — validated by a schedule-time dry
run when version pins one, and re-validated at every firing:
# nightly-patch.yaml
workflow_code: patch-infrastructure
cron_schedule: "0 2 * * 6"
repeat: true
inputs:
environments: [dev, test]
unattended: true
opschain scheduled-activities create demoenterprise -f nightly-patch.yaml
created scheduled activity 01a13c58-…-example (workflow patch-infrastructure, next run 2026-10-17T02:00:00.000000Z)
A repeating schedule pairs repeat: true with cron_schedule; a
one-shot sets run_at instead. -i key=value overlays the file's
inputs exactly like runs create, and a secret-typed input takes vault
references only — served masked everywhere it is read back. Edits and
removal address the id the create printed:
opschain scheduled-activities update 01a13c58-…-example -i unattended=false
updated scheduled activity 01a13c58-…-example (workflow patch-infrastructure, next run 2026-10-17T02:00:00.000000Z)
An update's inputs object (file or -i) replaces the stored one
wholesale, and update -f with enabled: false pauses the schedule
without losing it. Read the schedule back:
opschain scheduled-activities list
TYPE WHAT CRON ENABLED NEXT-RUN ID
scheduled_workflow patch-infrastructure 0 2 * * 6 true 2026-10-17T02:00:00Z 01a13c58-…-example
opschain scheduled-activities show <id> prints one entry's full
definition — inputs, overrides and all — and delete <id> is a hard
delete: its future firings never happen.
Firings leave an audit trail in the objects they create and the event
feed, never a log file: every run a schedule fires carries
initiated by scheduled_activity with the schedule's id (and
scheduled: true), so opschain runs list attributes the work; a
firing that cannot create its run records a
warn:scheduled_activities:error event on the activity's node —
disabling a one-shot, advancing a repeating schedule past the bad tick,
never crashing the sweep. Unless allow_parallel_execution says
otherwise, a due tick defers while the previous firing's run is still
live. And the reference is honoured both ways: a workflow still named by
a scheduled activity refuses admin workflows destroy by name — delete
the schedule first (lesson 4.4). The ownership warning from lesson 5.2
applies here too: create schedules as the everyday operator, never as
the bootstrap admin.
2. Inbound webhooks: external systems as a trigger source
The third source is inbound webhooks: an external system — GitHub, a CI server, anything that can POST signed JSON — fires your triggers directly. An administrator registers a connection, which serves a signed endpoint:
HOOK_SECRET=$(openssl rand -hex 32) opschain admin inbound-webhooks create ghmain \
--service-account ci-bot --secret-env HOOK_SECRET
Created inbound webhook connection ghmain (POST /api/hooks/ghmain)
Point the external system at <server>/api/hooks/ghmain with that same
secret. The endpoint is unauthenticated except the HMAC-SHA256
signature — the signature is the authentication: GitHub's own
X-Hub-Signature-256 shape works unchanged, and anything else can send
the generic X-OpsChain-Signature + X-OpsChain-Timestamp shape.
Replays are refused (a timestamp outside the connection's replay window,
or a repeated delivery id, answers a named 4xx plus a
warn:webhooks:refused event — a webhook failure is never silent),
payloads are capped at 1 MiB of JSON, and each connection carries its
own rate limit (opschain docs inbound-webhooks).
A verified delivery lands as one external:webhook:received event, and a
trigger binds to it with subject external plus conditions over the
delivery document:
# deploy_on_push.yaml
code: deploy_on_push
subject: external
events: [external:webhook:received]
conditions:
- event.data.connection eq ghmain
- event.data.event eq push
action_type: workflow_run
action_params:
workflow: release
inputs:
git_ref: ${{ event.data.payload.ref }}
opschain triggers create demoenterprise -f deploy_on_push.yaml
created trigger demoenterprise/deploy_on_push (01a13d02-…-example)
The raw payload rides under event.data.payload.*, so the templating
idiom you already know feeds it into the run's typed inputs. Prove the
wiring as lesson 5.2 taught: post one delivery, find the
external:webhook:received event's ID in opschain events, and dry-run
the trigger against it with triggers test.
One rule matters more than the rest: the firing executes as the
connection's service_account — not as you, not as the trigger's
owner, whatever run_as says. The administrator who registered the
connection chose exactly which identity external systems may act as, and
that account needs the ordinary create rights on the target (grant them
as lesson 7.2 teaches). A missing or disabled service account settles the
firing failed by name — never silently, never as someone else. The
GitHub-specific lane (installation, check runs) is lesson 13.1.
Both triggers and scheduled activities carry run_as and owned_by on
their write surfaces, and the GUI drives the same v2 calls
(TriggerService, ScheduledActivityService — opschain docs api-v2).
Check yourself
- Read your schedule back with
scheduled-activities show: which field tells you when it fires next, and what event records a tick that could not create its run? Then: what authenticates a call to/api/hooks/ghmain, and as whom does the resulting firing execute?
What you learned
- A scheduled activity is a flat YAML (
workflow_code,cron_scheduleorrun_at,repeat,inputs) created on a project;updatereplaces inputs wholesale andenabled: falsepauses it. - Scheduled runs are attributed
initiated by scheduled_activity; a tick that cannot fire recordswarn:scheduled_activities:error. - An inbound webhook connection is a signed endpoint
(
POST /api/hooks/<code>): the HMAC signature is the authentication, replays and oversize payloads are refused loudly. - A delivery is one
external:webhook:receivedevent; triggers bind to it with subjectexternaland execute as the connection's service account.
Next: lesson 5.4 — Custom events (opschain learn 5.4), or back to your track (opschain learn).