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)