You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Graph plan does not record a step's on: [...] filter, so graph show draws conditional edges as unconditional #537
nodus graph show (#534) draws every dependency edge as an unconditional arrow,
because the plan object it renders does not record a step's with { on: [...] }
dependency-outcome filter.
So this workflow:
workflow deploy {
step build { return "ok" }
step notify after build with { on: ["failed"] } { return "alerted" }
}
renders identically to one where notify runs on success. The diagram says
"notify depends on build", which is true, and implies "build then notify", which
is not.
The renderer deliberately does not guess — an unconditional arrow for a
conditional edge is a lie a diagram tells convincingly, and it is worse than no
diagram because it is persuasive. That decision is recorded in src/nodus/orchestration/graph_render.py's module docstring and in docs/runtime/TASK_GRAPHS.md § "What an edge means". This issue is about
removing the reason for it.
edges is a list of [source, target] pairs. The accepted-outcome set lives on
the task, is validated against JOIN_ON_STATES in orchestration/workflow_lowering.py, and is not carried into the plan.
Fix direction
Carry the filter on the edge. The shape that keeps existing consumers working is
an additional key rather than a change to edges:
"edge_conditions": {"build->notify": ["failed"]}
or edges as 3-tuples behind a separate key. edges is public-ish — the CLI, the
tests, and anything reading plan_workflow() output consume it — so changing its
element arity is the more disruptive option and needs a CHANGELOG entry either
way.
Then graph_render can label the edge (build -->|failed| notify in Mermaid, [label="failed"] in DOT) and the docstring caveat comes out.
Worth deciding at the same time: whether the defaulton set (the implicit
"completed") is rendered. Labelling every edge completed would be noise;
labelling only the non-default ones means the absence of a label carries meaning,
which needs saying in the docs.
Related
TASK_STATUSES is seven values; JOIN_ON_STATES is the three a dependency can
reach mid-run (completed, failed, skipped). Only the latter can appear on
an edge, which bounds the label vocabulary usefully.
nodus graph show landed on main after v5.1.0. The underlying gap — the plan
not recording on: — predates it and affects any consumer of plan_workflow()
output, not just the renderer.
Done in the same PR deliberately: marking when-guarded edges while leaving on: edges invisible would have been a marker on one path with a sibling path unmarked. Same information loss, same plan object, and a renderer that dashes one kind of conditional edge but not the other is arguably worse than one admitting it knows neither.
The plan carries both, as additional keys rather than a reshaped edges — the shape this issue asked for:
edges: [["build","notify"],["build","verify"],["build","done"]]
edge_conditions: {"build->notify": ["failed"]} # with { on: ["failed"] }
conditional_edges: [["build","verify"]] # when reached("flaky")
edges and levels are untouched, so the CLI, the tests and anything reading plan_workflow() keep working.
On the default-on question this issue raised: only non-default sets are recorded, so a plain solid arrow means on: ["completed"]. Labelling every edge completed would be noise — which means absence carries meaning, and that is now stated outright in TASK_GRAPHS.md rather than left for a reader to infer.
The renderer's refusal to guess is preserved in kind: _edge_styling() reads only what the plan states, so a plan stored before these keys existed still renders as plain arrows — which is exactly what it knows.
Activity
docs: log the three known limits from #534 as issues #535-#537 (#538)
workflow: a conditional edge says so, in the plan and the diagram (#471…
release: 5.3.0 (#559)
Masterplanner25 commentedon Aug 25, 2026
Closed by #557 (merged), alongside #471.
Done in the same PR deliberately: marking
when-guarded edges while leavingon:edges invisible would have been a marker on one path with a sibling path unmarked. Same information loss, same plan object, and a renderer that dashes one kind of conditional edge but not the other is arguably worse than one admitting it knows neither.The plan carries both, as additional keys rather than a reshaped
edges— the shape this issue asked for:edgesandlevelsare untouched, so the CLI, the tests and anything readingplan_workflow()keep working.Rendering:
On the default-
onquestion this issue raised: only non-default sets are recorded, so a plain solid arrow meanson: ["completed"]. Labelling every edgecompletedwould be noise — which means absence carries meaning, and that is now stated outright inTASK_GRAPHS.mdrather than left for a reader to infer.The renderer's refusal to guess is preserved in kind:
_edge_styling()reads only what the plan states, so a plan stored before these keys existed still renders as plain arrows — which is exactly what it knows.Shipped in v5.3.0 — PyPI · release notes.