Skip to main content
Version: v5.7.0

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_schedule or run_at, repeat, inputs) created on a project; update replaces inputs wholesale and enabled: false pauses it.
  • Scheduled runs are attributed initiated by scheduled_activity; a tick that cannot fire records warn: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:received event; triggers bind to it with subject external and execute as the connection's service account.

Next: lesson 5.4 — Custom events (opschain learn 5.4), or back to your track (opschain learn).