Upgrading to 0.18 — dependency conditions
Release 0.18.0 replaces 0.17.0’s per-dependency flags (continue_on_failure /
continue_when_skipped, read from the dependency, applied to every dependent
alike) with typed per-edge outcome acceptance: each depends_on entry
names exactly which of its dependency’s terminal outcomes satisfy it, plus
all/any grouping for shapes a flat list can’t express. This is a breaking
behaviour change on top of 0.17.0’s own breaking change — no schema
migration is involved, but existing workflows can skip or run steps
differently than they did on 0.17.x. Read this page before upgrading if any
workflow uses continue_on_failure on a step that has a depends_on, or
uses continue_when_skipped at all.
If you’re migrating directly from 0.16.x (never deployed 0.17.0), read the 0.17 upgrade guide first — this page assumes you’re starting from 0.17.0’s rules, not 0.16.x’s.
The model
Section titled “The model”Full design: docs/superpowers/specs/2026-10-01-dependency-conditions-design.md.
Full syntax and worked examples: the Conditionals guide.
In short:
continue_when_skippedis gone. A task with a step that still sets it gets a named, actionable error (continue_when_skipped was removed in 0.18.0; see the 0.18 upgrade guide...) instead of silently being ignored — at job creation (server) or before execution (stroem run), and fromstroem validate. It is not a parse error and does not fire just from loading the workspace — see the Checklist below.continue_on_failureis now self-scoped only. It means this step’s ownfailedstatus doesn’t fail the job. It no longer has any effect on whether a dependent runs.depends_onis now a tree. Each entry is a bare name (sugar foraccept: [completed]), a{step, accept}object, or anall/anygroup.acceptis a non-empty list drawn from five outcomes —completed/failed/cancelled/skipped/omitted— or the literalany(all five). A dependent now states directly what it tolerates about each dependency, instead of relying on the dependency to broadcast a flag to everyone downstream.- Readiness is uniform. The gate always waits for every step named
anywhere in a
depends_ontree to go terminal before deciding anything — including a tree that’s already conclusively unsatisfiable by one child with a sibling still pending, and ananygroup already satisfied by its first child. Neither case short-circuits.
Migration table
Section titled “Migration table”0.17.0’s two flags keyed off a dependency’s skip reason, not off why that
reason arose: continue_when_skipped tolerated the dependency being
skipped with reason condition, empty, or cascade;
continue_on_failure tolerated failed/cancelled/skipped with reason
unreachable or unknown. 0.18.0 collapses cascade and unreachable into
one unified omitted outcome — deliberately, because once a dependency is
itself blocked by its own tree, nothing further downstream can (or should)
tell whether the ultimate cause several hops back was a choice or a failure.
That collapse means two of the four migration cases are not exact
translations — the table below says so honestly rather than papering over it:
| Dependency’s 0.17.0 flags | Closest new accept |
What changes, precisely |
|---|---|---|
| neither | [completed] (unchanged default) |
Exact. |
| both | any |
Exact (both old flags together already tolerated everything but pending). |
continue_when_skipped only |
[completed, skipped] |
Narrows: today this also tolerates the dependency being cascade-skipped (an upstream choice-block reaching it); omitted isn’t in this set, so that case is now newly rejected. |
continue_on_failure only |
[completed, failed, cancelled, omitted] (any minus skipped) |
Widens: omitted here also covers the dependency being cascade-skipped, which the old flag never tolerated — only unreachable/unknown did. |
The “neither”/“both” rows are exact. The two single-flag rows are approximations, and you need to know which case you’re actually in before trusting them.
Worked example: where the single-flag rows go wrong
Section titled “Worked example: where the single-flag rows go wrong”flow: a: action: maybe-run when: "{{ false }}" # always skipped `condition`
b: action: middle-step depends_on: [a] continue_when_skipped: true # 0.17.0 flag, on the dependency
c: action: final-step depends_on: [b] # no flag at allUnder 0.17.0: a is skipped condition. b’s own continue_when_skipped
tolerates that, so b is cascade-skipped rather than blocked — and c, even
with no flag of its own, runs, because b’s skip is choice-class and b
itself carries the flag that lets dependents through. c runs.
Migrating literally via the table’s row 3 (continue_when_skipped only → accept: [completed, skipped]), applied to c’s edge to b, does not
reproduce this:
b’s own resulting outcome is notskipped— it’somitted.bwas blocked by its own tree (specifically, bya’s choice-skip), and §2.1’s whole point is that this is indistinguishable, fromc’s vantage point, frombbeing blocked by a failure several hops back.skippedis reserved for a step’s own directwhen/empty-loop choice —bnever made that choice itself,adid.- So
accept: [completed, skipped]onc’s edge tobdoesn’t includeomitted—cis now omitted too, where it used to run. - Reproducing 0.17.0’s behaviour here needs
accept: [completed, omitted](orany) instead — the opposite of what a literal reading of “migratecontinue_when_skippedtoaccept: [completed, skipped]” would suggest.
This is exactly why the table above is framed as “closest approximation,” not “exact translation.” Check which case you’re actually in — specifically, whether the chain between the flagged dependency and the step you’re updating has any intermediate hops — before applying a row mechanically.
When the distinction actually matters
Section titled “When the distinction actually matters”If a workflow genuinely needs “tolerate a failure several hops back, but not
a choice-skip several hops back” (or vice versa) through an intermediate
dependency, there is no single-edge-on-the-intermediate-dependency way to
express it — the intermediate’s own omitted outcome has already thrown
that distinction away. The only faithful fix is to stop relying on the
intermediate hop’s broadcast and name the actual origin directly: give the
edge several hops up an explicit accept for its own direct outcome
(failed/cancelled vs. skipped), and have the intermediate steps use
accept: any purely for ordering.
Structural-catch audit
Section titled “Structural-catch audit”0.17.0 had a recursive “caught somewhere downstream” rule: a failure at step
f didn’t fail the job if every downstream path from f eventually
passed through a step with continue_on_failure (f itself counted). That
mechanism has no replacement in 0.18.0 — continue_on_failure is now a
direct, self-scoped flag check only (f catches itself or it isn’t caught
at all; see the Conditionals guide).
If your workflows relied on a downstream step’s flag to catch an upstream failure — not the failing step’s own flag — the job’s pass/fail outcome changes. You need to audit for this before upgrading, and the audit has to walk flow definitions, not job history: a job that happened to complete under 0.17.0’s rule tells you nothing about whether the next run of the same shape would, since the rule depended on the flow graph, not on which rows actually ran.
Audit methodology — reproduce 0.17.0’s caught_steps() logic by hand (or
script it) against each task’s flow:
- For every step
f, find every step thatf’s outcome can reach viadepends_onedges (ignorewhen/for_each— this is a pure graph walk over the dependency edges). fwas “caught” under 0.17.0 ifffitself hascontinue_on_failure, OR every one off’s downstream paths to the end of the flow passes through some step withcontinue_on_failure— every path needs a catcher, not merely one. Getting this backwards understates how many workflows are actually relying on it.- For every
fthat was caught only by a downstream step’s flag (not its own), decide: does the job still need to endcompletedwhenffails? If yes, movecontinue_on_failureontofitself. If the downstream step merely needs to run afterffails (without changing the job’s outcome), that’s a readiness question instead — give that step’s edge tofanacceptthat includesfailed(orany), and leavecontinue_on_failureoff both steps.
No amount of accept tuning on downstream edges substitutes for step 3’s
first branch — accept only ever answers “do I run,” never “does the job
still count as succeeded.” The origin step still needs its own
continue_on_failure for that.
Rollout ordering (mixed replicas, mixed CLI)
Section titled “Rollout ordering (mixed replicas, mixed CLI)”Don’t merge workflow YAML using the new depends_on shapes ({step, accept},
all, any) until every server replica — and every stroem / stroem-api
CLI binary anyone might load that workspace with — is running 0.18.0+. A
pre-0.18 parser doesn’t reject the new shape outright; it silently mis-parses
it, and which failure mode you get depends on the step:
- Inline step (
type: ...): the old parser’sdepends_onfalls through a bare-name-list path with a.unwrap_or_default()-style fallback — a{step: a, accept: [...]}entry or anall/anygroup doesn’t deserialize as a plain string, so the wholedepends_onlist quietly becomes empty instead of erroring. The step becomes an unintended root: it runs immediately, with no dependency gate at all, instead of waiting for what the new YAML actually says. - Reference step (
action: ...): the old parser fails outright on the unrecognized shape, and — same as any other reference-step parse failure — the whole file is skipped with only a log warning: every task, action, and trigger it defines disappears from that replica (cron triggers silently stop firing, webhooks 404) until it catches up.
Helm’s default 2-replica RollingUpdate creates a real window where an old
and a new replica both serve traffic at once, and a developer’s local CLI can
easily lag the fleet by more than that window. Roll the server fleet and
upgrade every CLI binary first; only merge YAML using the new
depends_on shapes once every replica and every CLI that touches this
workspace is confirmed on 0.18.0+.
Checklist
Section titled “Checklist”- Confirm every server replica and every CLI binary (
stroem,stroem-api) that loads this workspace is on 0.18.0+ before merging YAML with the newdepends_onshapes — see Rollout ordering above. - Run
stroem validateover the whole workspace before upgrading — any lingeringcontinue_when_skipped, or a malformeddepends_ontree shape (an emptyall/anygroup, an empty or duplicateacceptlist), is now a named, actionable error naming the task and step. This is not a parse error and does not fire just from the server loading or reloading the workspace — a server only catches it when a job for the affected task is actually created (triggered, scheduled, re-run, restarted, or dispatched as atype: taskchild), and by then the trigger/schedule/ webhook/hook has already “fired and missed”: the job creation request fails with a 400 and a clear message, but a cron/webhook/event-source/ hook-sourced attempt has nowhere to surface that beyond the server log.stroem validate(orstroem runfor a single task) is the only way to catch this ahead of time, before anything tries to actually run it. - For every dependency that had
continue_when_skippedorcontinue_on_failureunder 0.17.0, find its dependents and give each edge the rightacceptset from the migration table above — checking the single-flag rows against the worked example if there’s more than one hop between the flagged step and the dependent. - Run the structural-catch audit over every
task’s flow. Any step caught only by a downstream
continue_on_failureneeds that flag moved onto the failing step itself if the job must still endcompleted. - Loop rollup: if a
for_eachplaceholder hascontinue_on_failureand at least one dependent relies on that placeholder rolling upcompleteddespite a tolerated instance failure — 0.17.0 and earlier hid the failure behind acompletedstatus; 0.18.0 always reports the true rollup status (failed, if any instance failed), and the output array is now built and visible on thatfailedrollup too. Update every such dependent’s edge to the placeholder toaccept: [completed, failed](orany). - Deduplicate any
depends_on: [a, a]-style duplicate entries — still valid YAML, but redundant, and the validator now rejects a duplicate name among the direct siblings of the sameall/anynode (the same name recurring across different branches with differentacceptsets is fine and unaffected). - If your dashboards or alerting read the
unreachableskip-reason by its UI label: the badge changes from “upstream failed” to the outcome-neutral “not satisfied” (and the step-detail explanation drops thecontinue_on_failurewording) in 0.18.0, because a step can now beomittedfor reasons that have nothing to do with an upstream step failing — e.g.{step: x, accept: [failed]}is unsatisfied, and the dependent omitted, precisely whenxsucceeds. This label change is retroactive: it applies to everyunreachablerow the UI renders, including rows on jobs that ran well before the upgrade, not just newly skipped steps — expect old jobs in your job list to show the new wording too once you’re running 0.18.0. Thecascadebadge (“upstream skipped”) is unchanged, but its explanation text is now framed as describing pre-0.18 rows specifically, sincecascadeis never written by new code. - Existing jobs in flight at upgrade time: a row already past the gate
(
ready,claimed,running, or terminal) is never re-gated. Only rows stillpendingare evaluated under the new tree/acceptmodel on their next cascade. Afor_eachplaceholder that had already rolled up before the upgrade (any status) keeps that historical status frozen as-is; a placeholder stillrunningat upgrade time rolls up under the new rule the first time it finishes afterward — two otherwise-identical loops can show different final statuses purely because of which side of the deployment they happened to finish on.
See also: Conditionals for the full accept/all/any
syntax and worked examples, and the 0.17 upgrade guide
if you’re migrating from 0.16.x and haven’t already applied that one.