Skip to main content

Dependency rules

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.

Precedence

When the planner computes an instance's effective dependency mode it walks this chain and stops at the first match:

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

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".

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

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 (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? (existing dependencyMode / condition)
  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.

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)