5.2 Triggers
triggers
After this you can: write a trigger, dry-run it against a real event, inspect its firings, and keep its definition under change control.
Before you start — tick each one:
- Lesson 5.1 — The record: events and chains (
opschain learn 5.1)
In the GUI: the Triggers tab on a project page, beside Scheduled activity — definitions, their state, and each trigger's firings.
A trigger is a standing rule: when an event like this happens,
then create this change or workflow run. Triggers are how the estate
starts running itself — promote on green, deploy on artifact, notify on
failure. By the end of this lesson you will have one trigger that fires
a workflow when a deploy succeeds, proven against a real event before it
ever ran, with its firings readable and its definition in git. You need
a licence with the triggers feature — every tier grants it, the Basic
tier included (opschain docs entitlements).
1. Anatomy of a trigger
A trigger has a match half and an action half:
- subject — what kind of thing to watch:
change,workflow_run,artifact,node, or a custom event's subject. - events — which lifecycle moments: the friendly vocabulary
(
created,started,finished,error,cancelled) or a customns:nametype (lesson 5.4). - scope_path — the estate subtree it watches; events outside never match.
- conditions — zero or more
event.<path> <op> <value>expressions, all of which must hold. - action_type + action_params — what to create: a
change(parent_path+action) or aworkflow_run(workflow, optionallyversion).
triggers create takes the definition as flat YAML whose keys mirror the
API attributes verbatim:
# promote_on_success.yaml
code: promote_on_success
description: run patch-infrastructure whenever a dev web deploy succeeds
subject: change
events: [finished]
scope_path: /projects/demoenterprise/environments/dev/assets/web
conditions:
- event.source.action eq web:deploy
action_type: workflow_run
action_params:
workflow: patch-infrastructure
opschain triggers create demoenterprise -f promote_on_success.yaml
created trigger demoenterprise/promote_on_success (01a12c58-cedb-7222-9e5d-7c58d20028c5)
opschain triggers list demoenterprise
CODE SUBJECT EVENTS SCOPE ENABLED
promote_on_success change finished /projects/demoenterprise/environments/dev/assets/web true
code is the trigger's per-project identity and is immutable once
created. A workflow_run trigger runs the workflow's latest published
version unless action_params.version pins one.
⚠️ Own your triggers as an everyday operator, not as the bootstrap admin. A firing executes as the trigger's owner (or its
run_asidentity), and the bootstrapadminaccount is break-glass: interactive-only, refusing the delegated firing-time identity resolution by design. An admin-owned trigger therefore fail-fires — every firing settlesfailedwith the owner-resolution error in its ERROR column, and no run is ever created. Create triggers (and scheduled activities) logged in as the everyday operator from lesson 0.1.
Running an automation as yourself is fine for your own workflow — but
anything a team depends on should run as a service account: a
durable non-human principal that survives you leaving. Mint one with
opschain admin service-accounts create deploy-bot, grant it access
through a group (lesson 7.2), and set it as the trigger's run_as (your
act_as trust grant authorises the delegation — lesson 7.3). Give the
trigger an owned_by group too, so maintenance and failure-notification
accountability routes to the team. When a person does leave, opschain admin users reassign-automations <username> moves everything their
identity executes onto a service account in one verb.
2. Conditions
Conditions address the matched event's document:
event.type,event.node_path,event.username— the event itself.event.data.<key>— the event's payload. Lifecycle events carry little here; custom and artifact events carry whatever was emitted.event.source.<…>— the sourcing object's context.event.source.actionis the sourcing change's action path — the usual discriminator for lifecycle triggers, as above: fire on this action finishing, not on every change under the scope.
3. Dry-run before you trust it
Would it fire — and if not, which gate stops it? triggers test
evaluates the trigger against any stored event with the live engine's own
match functions, writing nothing:
opschain triggers test demoenterprise/promote_on_success \
--event 01a12f20-ff86-706d-b8e0-f58cc746791d
trigger demoenterprise/promote_on_success against event 01a12f20-ff86-706d-b8e0-f58cc746791d:
enabled: yes
subject/event match: yes
scope match: no
condition event.source.action eq web:deploy ✗ actual: web:start:run
verdict: would not fire
Error: trigger demoenterprise/promote_on_success would not fire against event 01a12f20-ff86-706d-b8e0-f58cc746791d
One line per gate, each condition with the value the engine actually saw, then the resolved action params (or the template error) and the verdict. The exit code is 0 only on "would fire", so the check scripts cleanly.
--event takes an event id — the ID column of opschain events —
not a correlation id. Pass a correlation id and the event lookup simply
misses; the two columns name different things (lesson 5.1).
4. Inspect, evolve, pause
opschain triggers show demoenterprise/promote_on_success
code: promote_on_success
id: 01a12c58-cedb-7222-9e5d-7c58d20028c5
name: promote_on_success
description: run patch-infrastructure whenever a dev web deploy succeeds
enabled: true
subject: change
events: finished
scope_path: /projects/demoenterprise/environments/dev/assets/web
conditions:
- event.source.action eq web:deploy
action_type: workflow_run
action_params:
{
"workflow": "patch-infrastructure"
}
throttle_per_minute: 60
created_by: opschain
created_at: 2026-10-12T06:03:00.052539Z
updated_at: 2026-10-12T06:41:15.657397Z
newest firings:
WHEN STATUS CREATED ERROR
2026-10-12T00:46:35.859865Z created WorkflowRun/01a12d93-6190 -
triggers update sends only what you supply — everything else keeps its
stored value. Two flag shortcuts cover the common edits:
opschain triggers update demoenterprise/promote_on_success --throttle 1
opschain triggers update demoenterprise/promote_on_success \
--condition "event.source.action eq web:deploy" \
--condition "event.data.exit_status eq 0"
updated trigger demoenterprise/promote_on_success
The repeated --condition list replaces the stored conditions array
(it does not append); -f file.yaml updates every attribute the file
carries, except the immutable code. And pausing beats deleting:
opschain triggers disable demoenterprise/promote_on_success
opschain triggers enable demoenterprise/promote_on_success
opschain triggers delete demoenterprise/promote_on_success # firings go with it
enabled trigger demoenterprise/promote_on_success
5. Firings: the audit trail
Every time a trigger matches an event, the outcome is recorded as a firing — exactly once per (trigger, event), never a silent drop:
created— the change or workflow run it made, named in the CREATED column.suppressed— matched but deliberately not acted: throttled (throttle_per_minute), the chain depth cap (a self-triggering loop settles suppressed at depth 25 with awarn:triggers:depth_capevent), or discarded by an operator (lesson 5.6).failed— the action could not be created (an unresolvable template, a denied authorisation); the ERROR column names why. These are your dead letters: work that should exist and does not.deferred— parked by a maintenance window (lesson 5.6).
opschain triggers firings demoenterprise/promote_on_success --since 24h
opschain triggers firings demoenterprise/promote_on_success --status failed
opschain triggers firings demoenterprise/promote_on_success --deferred
opschain triggers firings demoenterprise/promote_on_success --reverse --wide
WHEN STATUS CREATED ERROR
2026-10-12T00:46:35.859865Z created WorkflowRun/01a12d93-6190 -
2026-10-11T22:10:08.114930Z failed - workflow patch-infrastructure: not found or not readable
A firing also rides the matched event's chain as an
api:triggers:fired hop, with the created run one depth level down
marked initiated by trigger — chains show displays automation's
decisions right in the cascade (lesson 5.1).
6. Export and import: definitions under change control
The database is the single source of truth for triggers, but definitions
belong in git for backup, review and promotion between servers.
triggers export emits a deterministic interchange document (stable key
order, byte-identical on re-export) in a nested when/then format:
opschain triggers export demoenterprise/promote_on_success --out triggers/promote_on_success.yaml
exported trigger demoenterprise/promote_on_success to triggers/promote_on_success.yaml
trigger: promote_on_success
description: run patch-infrastructure whenever a dev web deploy succeeds
enabled: true
when:
subject: change
events:
- finished
scope: /projects/demoenterprise/environments/dev/assets/web
conditions:
- event.source.action eq web:deploy
then:
create: workflow_run
with:
workflow: patch-infrastructure
opschain triggers import demoenterprise -f triggers/promote_on_success.yaml
opschain triggers import demoenterprise -f triggers/promote_on_success.yaml --update
updated trigger demoenterprise/promote_on_success from triggers/promote_on_success.yaml
Import creates; --update replaces an existing code's definition in
place (the document fully replaces the stored definition, defaults
included), through the same authorised path a GUI edit uses. Note the two
formats are deliberately distinct: the nested when/then document is
export/import's interchange format, the flat attribute YAML is
create -f's — each loader rejects the other, naming the right command.
Lesson 8.2 builds the full change-control discipline on these round
trips.
Check yourself
- Dry-run your trigger against the
api:changes:successevent from lesson 5.1: which gate fails, and what does the report print beside the condition? Then: where is the record of a firing the trigger could not act on, and what status does it carry? - Does
opschain triggers firings acme/<trigger>show a firing with STATUScreated— and who executed it? The Academy'strigger-firesexercise readsapi:triggers:createthenapi:triggers:firedon your trigger; a trigger owned by the bootstrap admin fail-fires, so create it as the everyday operator (or set--run-as, lesson 7.3). - Verified version:
opschain academyruns thetrigger-firesexercise against this lesson.
What you learned
- A trigger is subject × events × scope × conditions → a change or
workflow run, created from flat YAML with
triggers create -f, owned by an everyday operator or run as a service account. triggers test --event <event-id>reports every gate with the value the engine saw and exits 0 only on "would fire".- Every match is a firing —
created,suppressed,failedordeferred— readable withtriggers firingsand visible on the chain as anapi:triggers:firedhop. export/import [--update]round-trip the nestedwhen/theninterchange document for backup and promotion.
Next: lesson 5.3 — Time and the outside world: schedules and inbound webhooks (opschain learn 5.3), or back to your track (opschain learn).