Skip to main content
Version: v5.6.1

5.4 Custom events

Included from Basic tiertriggers

After this you can: emit events in your own vocabulary from a step or a script, read what the stored event carries, and trigger on it.

Before you start — tick each one:

  • Lesson 5.2 — Triggers (opschain learn 5.2)
  • Lesson 2.1 — Your first action (opschain learn 2.1)

In the GUI: custom events appear in the Activity tab and the Audit history search under their own type, filterable like any other.

Lifecycle events say a change finished; they cannot say the database cluster is provisioned or the smoke test found three regressions. Custom events let your actions speak in your domain's vocabulary — and lesson 5.2's triggers fire on them exactly as on built-in events. By the end you will have emitted one from a step, found it in the feed with its data, and dry-run a trigger against it.

1. Types are namespaced — and some prefixes are the server's​

A custom event type is <namespace>:<name> — database:provisioned, smoketest:regressions_found. Pick a namespace per domain and keep it; triggers match on the full type.

Five prefixes are reserved for the server's own vocabulary: api:, system:, info:, error:, warn:. Emitting one is refused — a named error at action load for a declarative emit: step, a step failure at runtime for step emit-event — so your events can never impersonate platform lifecycle.

2. The emit: step kind​

A YAML action emits declaratively with an emit: step, whose with: map is the event's data — templated with the same ${{ … }} expressions as run: commands:

# .opschain/actions/database/provision.yaml — defines database:provision
action: provision
events:
emits:
- database:provisioned
steps:
- name: apply
run: terraform apply -auto-approve
- emit: database:provisioned
with:
cluster: "${{ properties.cluster }}"
tf_exit: "${{ steps.apply.outputs.exit_status }}"

The emit step is an ordinary step in the tree: it runs after apply, and its event is recorded only when it executes successfully — a failed run never announces success. The events.emits header is advisory metadata (GUI pickers, trigger lint see it); it never gates what a step may emit.

Note the with: key does double duty in the schema: on a uses: step it binds a composed action's inputs (lesson 2.2); on an emit: step it is the event's templated data.

3. opschain step emit-event — from any script​

Inside any running step — hostexec or containerexec — the step subcommand emits imperatively:

opschain step emit-event database:provisioned \
--data cluster=prod-a \
--data-file details=@tf-summary.json
  • --data key=value adds a string entry (repeatable).
  • --data-file key=@path reads the entry's value from a file — embedded as parsed JSON when the content is valid JSON, as a string otherwise. This is how a structured report (a terraform summary, a scan result) becomes queryable event.data for trigger conditions.
  • The event is recorded when the step succeeds, like the declarative kind. Running the subcommand outside a step is a usage error.

4. What the stored event carries​

However it was emitted, the event is sourced from the emitting step, lands on the change's node path, and inherits the change's correlation chain — so chains show places your domain announcements right in the cascade that produced them, and anything a trigger creates from them goes one depth level deeper on the same chain. Run the action and read the row back:

opschain runs create change acme/environments/dev/assets/db --action database:provision --follow
opschain events --filter type=database:* --tail 1
WHEN ID TYPE NODE USER CORRELATION DETAIL
2026-10-12T09:40:12.551204Z 01a14701-…-example database:provisioned /projects/acme/environments/dev/assets/db alice 01a14700-…-example -
opschain events show 01a14701-…-example
id: 01a14701-…-example
type: database:provisioned
node path: /projects/acme/environments/dev/assets/db
user: alice
system: false
correlation id: 01a14700-…-example
chain depth: 1
source: step 01a14702-…-example
created at: 2026-10-12T09:40:12.551204Z
payload:
{
"cluster": "prod-a",
"tf_exit": "0"
}

The event's data passes the run's secret masker before storage: a value the run knows to be secret is stored as ******, never in the clear. Masking is a backstop, not a channel — don't put secrets in event data; hand over a vault reference or an id instead.

5. Trigger on it​

A custom type goes in a trigger's events list like any lifecycle name, and conditions query the emitted data:

# deploy_app_on_db_provisioned.yaml
code: deploy_app_on_db_provisioned
subject: change
events: [database:provisioned]
scope_path: /projects/acme/environments/dev
conditions:
- event.data.cluster eq prod-a
action_type: change
action_params:
parent_path: /projects/acme/environments/dev/assets/web
action: web:deploy
opschain triggers create acme -f deploy_app_on_db_provisioned.yaml
created trigger acme/deploy_app_on_db_provisioned (01a14710-…-example)

Prove the wiring before relying on it: dry-run the trigger against the stored event's ID from section 4 —

opschain triggers test acme/deploy_app_on_db_provisioned --event 01a14701-…-example
trigger acme/deploy_app_on_db_provisioned against event 01a14701-…-example:
enabled: yes
subject/event match: yes
scope match: yes
condition event.data.cluster eq prod-a ✓ actual: prod-a
action params: {
"action": "web:deploy",
"parent_path": "/projects/acme/environments/dev/assets/web"
}
verdict: would fire

— then watch real firings with opschain triggers firings acme/deploy_app_on_db_provisioned --since 1h.

Check yourself​

  • Emit database:provisioned once more with a different cluster value and dry-run the trigger against that event: which line of the report changes, and what does the verdict become? Which five prefixes could you never have used for the type?

What you learned​

  • Custom event types are namespaced ns:name; the api: system: info: error: warn: prefixes are the server's and are refused at load or at runtime.
  • The emit: step kind announces declaratively, with with: as ${{ … }}-templated event data; events.emits is advisory metadata.
  • opschain step emit-event --data k=v --data-file k=@path emits from any step script; either way the event records on step success, sourced from the step, on the change's chain, masked by the run's secret masker, and events show prints its payload.
  • Triggers match custom types in events: and query the payload with event.data.<key> conditions — dry-run with triggers test first.

Next: lesson 5.5 — Notifications: watches, channels, the bell (opschain learn 5.5), or back to your track (opschain learn).