Trigger bindings
Which events may touch which environments is a platform standard: pull requests may validate, merges to main may verify staging, only a release tag may touch production. Trigger bindings write that standard down in intent.yaml, so it is one reviewed declaration instead of if: expressions scattered across CI files. The plan records which binding activated each environment.
Trigger bindings map CI provider events to environment activation. They let you declare which environments should be planned for each type of event — pull requests activate development, pushes to main activate staging, release tags activate production.
How it works
Provider event → TriggerBinding match → Environment activation → Plan
When orun plan runs with a trigger context (--trigger or --from-ci), it:
- Normalizes the raw provider event into a standard format
- Matches the event against declared trigger bindings
- Activates only environments that reference the matched triggers
- Plans only components subscribed to those active environments
Without a trigger context, all environments are planned as before.
Declaring trigger bindings
Trigger bindings live under automation.triggerBindings in your intent file:
automation:
triggerBindings:
github-pull-request:
description: Pull request validation
on:
provider: github
event: pull_request
actions:
- opened
- synchronize
- reopened
baseBranches:
- main
plan:
scope: changed
base: pull_request.base.sha
head: pull_request.head.sha
github-push-main:
description: Main branch verification
on:
provider: github
event: push
branches:
- main
plan:
scope: changed
base: before
head: after
github-tag-release:
description: Release planning
on:
provider: github
event: push
tags:
- "v*"
plan:
scope: full
Activating environments
Environments reference trigger bindings through activation.triggerRefs:
environments:
development:
activation:
triggerRefs:
- github-pull-request
parameterDefaults:
"*":
namespacePrefix: dev-
staging:
activation:
triggerRefs:
- github-push-main
parameterDefaults:
"*":
namespacePrefix: stg-
production:
activation:
triggerRefs:
- github-tag-release
parameterDefaults:
"*":
namespacePrefix: prod-
When github-pull-request fires, only development is activated. Components subscribed to development are planned; components only subscribed to staging or production are skipped.
Event matching
Each trigger binding declares match criteria under on:
| Field | Purpose |
|---|---|
provider | Event provider (required, e.g. github) |
event | Event name (required, e.g. push, pull_request) |
actions | Optional action filter (e.g. opened, synchronize) |
branches | Optional source branch filter with glob support |
baseBranches | Optional base branch filter (for PRs) with glob support |
tags | Optional tag filter with glob support |
All specified fields must match for the trigger to fire. Unspecified fields are not checked.
Glob patterns
Branch and tag filters support simple glob patterns:
branches:
- main # exact match
- release/* # prefix match (release/1.0, release/hotfix)
- feature/** # same as feature/*
tags:
- "v*" # matches v1.0.0, v2.3.1-rc1
- "*" # matches any non-empty value
Plan scope
Each trigger binding can set plan.scope to control how the plan is scoped:
| Scope | Behavior |
|---|---|
full | Plan all components in activated environments |
changed | Plan only changed components (enables --changed mode) |
selected | Plan exactly the components and environments the event names |
When scope: changed is set, the trigger also needs base and head to determine the diff range. These are dot-paths into the raw event payload:
plan:
scope: changed
base: pull_request.base.sha # resolves from event JSON
head: pull_request.head.sha
For push events, the standard paths are before and after.
Selected plans: an event that names what to plan
A workflow_dispatch event — a person or a platform asking for one component in
one environment — carries its selection in its inputs. scope: selected makes
that selection the whole plan, so the same plan command serves pull requests,
pushes and dispatches:
automation:
triggerBindings:
github-dispatch:
description: A deploy asked for by a person
on:
provider: github
event: workflow_dispatch
branches: [main]
plan:
scope: selected
components: inputs.component # dot-path: "api, web" or ["api", "web"]
environments: inputs.environment # dot-path: "stage"
autopush: false # no plan-time catalog push for these plans
# one command for every event; a dispatch plans its inputs, not a diff
orun plan --from-ci github --event-file "$GITHUB_EVENT_PATH" --changed --output plan.json
componentsandenvironmentsare dot-paths into the event payload, resolved likebaseandhead. A value is a comma-separated string (items trimmed) or a list of strings. Both are required withscope: selectedand invalid with any other scope.- The selection is exact and never empty. The plan fails, naming the cause, on a missing or empty value, an unknown component, a wildcard in a name, an environment the trigger does not activate, or a component that does not subscribe to a selected environment.
--changedand--filesare ignored for a selected plan (orun says so on stderr): the request is the scope.- Under
--trigger github-dispatchthere is no event, so the selection comes from--env(required) and--component:orun plan --trigger github-dispatch --env stage --component api. autopush: falseskips the best-effort catalog push thatexecution.state.autopushCatalogwould otherwise make after the plan; it is valid on any binding.--push-catalogstill pushes.- The plan records the trigger's scope as
selected; the object model records the revision asfull.
Opt-in lanes: requireRule
With plan.requireRule: true, a lane (one component in one environment) is
planned under the binding only if one of its profileRules names the binding.
The lane opts in, and the rule's profile is what it runs — deploy in stage,
verify in dev (Profile rules → Opting a lane in).
github-dispatch:
on: { provider: github, event: workflow_dispatch, branches: [main] }
plan:
scope: selected
components: inputs.component
environments: inputs.environment
requireRule: true
- Under
scope: selectedit is strict: every selected lane must have opted in, or the plan fails naming each lane that did not and the fix (not enabled for github-dispatch: web.stage — add a profileRule with when.triggerRef: github-dispatch to the lane's subscription). - Under another scope it drops the lanes that did not opt in, and fails only when none is left.
- Unset (the default) keeps today's behaviour: a lane with no rule naming the binding runs its default profile.
- The catalog records each opted-in lane as
spec.environments.<env>.manual: { trigger, profile }(the firstrequireRulebinding by name, with its rule's profile). A component that opts nothing in keeps its manifest hash. Inline intent components do not carry profile rules into the catalog, so they get nomanualfield.
CLI usage
Named trigger simulation (local development)
orun plan --trigger github-pull-request
This activates the named trigger directly without requiring a real event. Useful for local reproduction of CI behavior.
Provider event file (CI)
orun plan \
--from-ci github \
--event-file "$GITHUB_EVENT_PATH"
This reads the raw event JSON, normalizes it, matches against all declared trigger bindings, and activates the corresponding environments.
Combining with --env
When both a trigger and --env are provided, the result is the intersection:
orun plan --trigger github-pull-request --env development
If the specified environment is not activated by the trigger, planning fails with an error.
Multiple matched triggers
When an event matches multiple trigger bindings, orun:
- Collects all matched trigger names (sorted alphabetically)
- Activates the union of all referenced environments
- Checks for conflicting
plan.scopevalues — if triggers disagree, planning fails with a clear error
Plan metadata
When a trigger is used, the resulting plan includes trigger metadata:
{
"metadata": {
"trigger": {
"mode": "event-file",
"provider": "github",
"event": "pull_request",
"action": "synchronize",
"matchedBindings": ["github-pull-request"],
"activeEnvironments": ["development"],
"scope": "changed",
"base": "abc123",
"head": "def456"
}
}
}
Validation
Static validation (orun validate) checks:
- Trigger bindings have required
on.providerandon.eventfields plan.scopeisfull,changedorselected(if set);plan.componentsandplan.environmentsare required withselectedand invalid with any other scope, and each is a dot-path with no empty segment (also checked byorun plan)- Environment
triggerRefspoint to existing trigger bindings - No duplicate refs within an environment
A warning (not error) is produced for trigger bindings that no environment references.
Backwards compatibility
Existing intent files without automation or activation continue to work unchanged. The trigger system is opt-in:
orun planwithout--triggeror--from-ciignores all activation rules- Environments without
activation.triggerRefsare never activated by triggers (but are always included in non-trigger plans)