Skip to main content

Dependency rules

Ordering is a standard too: the database migrates before the API deploys, but on a pull request nothing needs to wait. Dependency rules declare, per environment or per trigger, whether an edge blocks, is advisory, or is dropped, so the plan decides what waits rather than a person at deploy time.

Dependency rules let you control whether dependsOn edges in the plan DAG are enforced (block execution), advisory (recorded but non-blocking), or disabled (omitted entirely), conditional on which trigger fired.

This is the dependency-graph equivalent of Profile rules: profile rules change what steps run inside a job, dependency rules change whether one job waits for another.

Motivation​

For pull-request validation you usually want fast, parallel feedback — there is no point waiting for database to plan before api can plan, because nothing is being applied. For merges to main or tag-based releases, the dependency must be enforced because real apply ordering matters.

Without dependency rules you'd either:

  • duplicate environments per trigger (verbose, drift-prone), or
  • mutate the DAG at execution time (breaks the "plan is the audit artifact" model).

Dependency rules keep the policy at plan time so orun plan --view dag shows the resulting graph deterministically.

Modes​

ModeDAG behaviorTypical use case
enforcedNormal dependsOn edges (blocks executor)Merges, releases, production deploys
advisoryEdges recorded as advisoryDependsOn metadata, not blockingPR validation, speculative previews
disabledDependency edge omitted entirelyEmergency bypass / explicit independence

enforced is the built-in default.

Syntax​

Environment-level default​

environments:
dev-preview:
activation:
triggerRefs: [github-pull-request]
dependencyMode: advisory

staging:
activation:
triggerRefs: [github-push-main]
dependencyMode: enforced

Component-subscription override​

spec:
subscribe:
environments:
- name: dev-preview
profile: plan-only
dependencyMode: advisory
dependencyRules:
- mode: enforced
when:
triggerRef: github-push-main
- mode: enforced
when:
triggerRef: github-tag-release

dependencyMode is the fallback when no rule matches; dependencyRules is an ordered first-match-wins list.

Per-edge override (dependsOn[].mode)​

dependencyMode and dependencyRules set the mode for every dependsOn edge of a component in a lane. When one lane needs a mix — a worker's production deploy must wait on its migration and its infrastructure, but not on peer workers it only calls at run time — set mode on the individual dependsOn item:

metadata:
name: api-worker
spec:
dependsOn:
- component: db-migrate
mode: enforced # always wait for the migration
- component: platform-terraform
mode: enforced
- component: billing-worker
mode: advisory # recorded, never blocks the deploy
subscribe:
environments:
- name: production
dependencyMode: enforced # the lane default for any other edge

mode accepts the same values as dependencyMode (enforced, advisory, disabled) and overrides the lane-level mode for that edge only; an edge without mode follows the lane exactly as before. It works in both directions: an advisory edge inside an enforced lane runs in parallel, and an enforced edge inside an advisory lane (for example a PR preview) still blocks.

An edge can also switch mode by trigger with modeRules, which mirrors dependencyRules (ordered, first match wins, when.triggerRef must exist in automation.triggerBindings). When no rule matches, the edge's mode applies, and when that is unset too, the lane-level mode:

dependsOn:
- component: db-migrate
mode: advisory # PR previews do not wait on migrations
modeRules:
- mode: enforced
when:
triggerRef: github-push-main

Precedence​

When the planner computes the effective mode of a dependsOn edge it walks this chain and stops at the first match:

  1. dependsOn[].modeRules[] whose when.triggerRef matched the active trigger
  2. dependsOn[].mode
  3. subscription.dependencyRules[] whose when.triggerRef matched the active trigger
  4. subscription.dependencyMode
  5. environment.dependencyMode
  6. built-in default enforced

Steps 3–6 are the instance's lane-level mode; steps 1–2 apply to one edge only.

The selected source is recorded on every job for auditability:

{
"dependencyMode": "advisory",
"dependencySource": "subscription-rule",
"dependencyRuleTriggerRef": "github-pull-request"
}

dependencySource can be "default", "environment", "subscription", or "subscription-rule". These fields describe the lane-level mode; an edge whose own mode overrides it is still visible in the job's edge lists — an advisory edge lands in advisoryDependsOn, an enforced one in dependsOn, a disabled one in neither.

Plan output​

For a pull-request plan with advisory mode the job retains both views:

{
"id": "api.dev-preview.verify",
"dependsOn": [],
"advisoryDependsOn": ["database.dev-preview.verify"],
"dependencyMode": "advisory",
"dependencySource": "subscription-rule",
"dependencyRuleTriggerRef": "github-pull-request"
}

For the same component on push-to-main:

{
"id": "api.staging.verify",
"dependsOn": ["database.staging.verify"],
"dependencyMode": "enforced",
"dependencySource": "environment"
}

orun plan --view dag annotates the environment header with the mode and --view dependencies distinguishes blocking vs advisory edges:

└─ api (api/dev-preview)
├─ depends-on: shared-secrets.dev-preview.verify
└─ advisory: database.dev-preview.verify
mode: advisory (rule:github-pull-request)

Validation​

Validated at plan time (not run time):

RuleReason
dependencyMode must be enforced, advisory, or disabledCatch typos before they suppress edges
dependencyRules[].mode is required and must be validFirst-match-wins must not produce empty modes
dependencyRules[].when.triggerRef must exist in automation.triggerBindingsAvoid silent fall-through
dependsOn[].mode must be enforced, advisory, or disabled when setSame as dependencyMode
dependsOn[].modeRules[].mode is required and valid; when.triggerRef must existSame as dependencyRules

Run orun validate to surface these errors.

Relationship to other features​

ConcernMechanism
Which environments are active for a triggerenvironments[].activation.triggerRefs
Which profile runs inside an active environmentsubscribe.environments[].profile + profileRules
Which dependsOn edges block executiondependencyMode / dependencyRules, per edge dependsOn[].mode / modeRules (this page)

Keeping these axes independent keeps the compiled plan DAG the single source of truth.

When not to use dependency rules​

  • If components are genuinely independent in all contexts, remove the dependsOn declaration instead of marking it disabled.
  • If you want different steps, use profile rules — dependency rules do not change what runs, only what waits.
  • For sequencing across environments (e.g. dev before staging) use environment.promotion.dependsOn, which is its own promotion-aware mechanism.

Include policy (plan selection)​

Since v2.9.0, dependsOn separates two orthogonal questions:

  1. Ordering — should A wait for B when both are in the plan? (dependencyMode and the dependency rules above)
  2. Inclusion — should B be pulled into the plan when only A was selected by --changed? (new include)

The new include field controls inclusion:

ValuePlan selection behaviorBest for
if-selectedOnly add an ordering edge if the dependency is already in the planDefault — most component dependencies
alwaysPull the dependency into the plan, then add the ordering edgeMigrations, codegen, shared infra, parent package build

if-selected is the built-in default and is applied during normalization. Change-detection no longer silently includes unchanged components.

Default: order-only​

metadata:
name: web-console
spec:
type: cloudflare-pages-turbo
dependsOn:
- component: ui-package
# include: if-selected (default)
changed: web-console -> plan: web-console
changed: ui-package -> plan: ui-package
changed: web-console + ui-pkg -> plan: ui-package -> web-console
full plan -> plan: ui-package -> web-console

Opt-in: always pull the dependency in​

metadata:
name: api-edge-worker
spec:
type: cloudflare-worker-turbo
dependsOn:
- component: identity-schema
include: always
reason: api requires latest generated identity contract before deploy
changed: api-edge-worker -> plan: identity-schema -> api-edge-worker

reason is free-form text surfaced for auditability; it never affects behavior.

Input edges (build-input rescope)​

Since v2.22.0, dependsOn answers a third orthogonal question:

  1. Rescope — should A be treated as changed when only B changed? (new input)

Mark a dependency input: true when its sources are build inputs of your artifact — a bundled shared package, generated contracts, anything your build compiles in. Change detection then treats a change to the dependency as a direct change to your component: --changed selects it, plans its jobs, and (on push-to-main lanes) schedules its deploy.

metadata:
name: policy-worker
spec:
type: cloudflare-worker-turbo
dependsOn:
- component: policy-engine
input: true # the worker bundles packages/policy-engine
changed: packages/policy-engine -> plan: policy-engine + policy-worker

Without input, this is the classic shared-package gap: the worker's bundle embeds the package, but --changed only matches files under the worker's own directory — so a package-only change ships no deploy and production serves a stale artifact until someone touches the worker's directory by hand.

input is transitive over other input edges: with contracts ←(input)─ sdk ←(input)─ console, a change to contracts rescopes both sdk and console. It composes with include (forward pull vs reverse rescope) and is inert to ordering (dependencyMode still decides what waits for what). Plain runtime peers — service bindings, API calls — should not be marked input: they appear in the blast radius (orun catalog affected) but a peer redeploy is not required for a peer-only change.

Input edges connect components. For files that belong to no component — a root lockfile, turbo.json, a shared tooling/ tree — declare them on the component with spec.change.inputs globs; a match is a direct change that then rides input edges the same way.

orun catalog affected --explain records the provenance of every rescope:

explain:
- ns/repo/policy-engine: input file changed: packages/policy-engine/src/index.ts
- ns/repo/policy-worker: build input changed (dependsOn input:true)

Relationship to dependencyMode​

include and dependencyMode are orthogonal — combine freely:

dependencyModeincludeEffect
enforcedif-selectedDefault. Blocking edge, only present if both ends are selected.
enforcedalwaysDependency is pulled in and acts as a hard ordering edge.
advisoryif-selectedPR pattern: parallel feedback, no transitive selection of unchanged components.
advisoryalwaysPulls the dependency in but records the edge as advisory rather than blocking.
disabled(any)Edge omitted from the plan entirely.

Validation​

Invalid include values fail at plan time:

component api: dependsOn[0].include "sometimes" is invalid
(expected "if-selected" or "always")

Missing-dependency errors are now scoped to include: always. Under the default if-selected, a dependency target that isn't in the plan is just a silently-dropped order edge — exactly what --changed wants. With include: always it remains a real misconfiguration:

dependency not found: api.pr depends on db.pr (include: always)