Skip to content

Graph plan does not record a step's on: [...] filter, so graph show draws conditional edges as unconditional #537

Closed

Description

@Masterplanner25

Summary

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.

Where the information is lost

plan_graph() / _step_plan_from_graph() return:

{"workflow": ..., "graph_id": ..., "nodes": [...], "edges": [[a, b], ...],
 "levels": [...], "parallel_groups": [...], "tasks": {...}}

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 default on 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

Affected versions

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.

Activity

  1. added a commit that references this issue on Aug 25, 2026
  2. Masterplanner25 commented on Aug 25, 2026

    @Masterplanner25
    OwnerAuthor

    Closed by #557 (merged), alongside #471.

    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.

    Rendering:

    n0 -->|failed| n1      n0 -> n1 [label="failed"];
    n0 -.-> n2             n0 -> n2 [style=dashed];
    n0 --> n3              n0 -> n3;
    

    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.


    Shipped in v5.3.0 — PyPI · release notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions