affaan-m/ECC · error · ValueError
Dependency cycle
Error message
Dependency cycle
What it means
validate_graph walks each node's dependency chain recursively via visit(); when a node name is already on the current traversal stack, the graph contains a circular depends relationship, so ValueError('Dependency cycle') is raised to prevent infinite recursion and an unsatisfiable workflow ordering.
Solutions
- Inspect the workflow JSON's node depends arrays and remove the edge that closes the loop (typically the most recently added one)
- Topologically list intended stages on paper (distill -> apply) and confirm each depends entry only points to earlier stages
- Rename a node instead of re-declaring it if the cycle came from duplicating an existing stage to add a variant
- If a shared computation is needed by two branches, extract it into a third node that both depend on rather than making the branches depend on each other
Example fix
// before (cycle)
{"nodes": {"apply": {"depends": ["distill"]}, "distill": {"depends": ["apply"]}}}
// after (acyclic)
{"nodes": {"distill": {"depends": []}, "apply": {"depends": ["distill"]}}} Defensive patterns
Strategy: validation
Validate before calling
def is_acyclic(nodes):
state = {}
def dfs(n):
if state.get(n) == 1: return False
if state.get(n) == 2: return True
state[n] = 1
for d in nodes[n].get('depends', []):
if not dfs(d): return False
state[n] = 2
return True
return all(dfs(n) for n in nodes) Try / catch
try:
graph = load_graph(kind)
except ValueError as e:
if 'Dependency cycle' in str(e):
# surface the offending workflow file and abort config load
... Prevention
- Keep workflows strictly staged: each node depends only on earlier stages
- Lint workflow JSONs with a topological sort in CI before shipping
- Avoid copy-pasting node blocks between workflow files without re-checking depends
When it happens
Trigger: A taste-*.json workflow graph defines nodes where node A depends on B and B (directly or transitively) depends again on A — e.g. 'apply' depends on 'distill' and 'distill' depends on 'apply'.
Common situations: Hand-editing a workflow JSON and adding a depends entry to reuse another node's output; copy-pasting node definitions between workflows so mutual references survive; renaming nodes so an intended dependency now resolves to an ancestor.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- references undeclared dependency
- Unknown output node
- Unknown workflow input
- Unwired workflow inputs
- agent profile inheritance cycle
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/417de95084de12ea.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/workflow_graphs.py:90
def validate_graph(graph):
"""Verify dependencies and every declared input; endpoint schemas need live QA."""
contents = graph['contents']
nodes, inputs = contents['nodes'], contents['schema']['input']
used = set()
for name, node in nodes.items():
if node['id'] != name:
raise ValueError(f'Node id mismatch: {name}')
for dependency in node.get('depends', []):
if dependency != 'input' and dependency not in nodes:
raise ValueError(f'Unknown dependency: {dependency}')
ancestors = {}
def visit(name, stack):
if name == 'input':
return set()
if name in stack:
raise ValueError('Dependency cycle')
if name not in ancestors:
deps = nodes[name].get('depends', [])
ancestors[name] = set(deps).union(*(visit(dep, stack | {name}) for dep in deps))
return ancestors[name]
for name, node in nodes.items():
reachable = visit(name, set())
for ref in references_in({'input': node.get('input'), 'fields': node.get('fields')}):
if ref[0] not in reachable:
raise ValueError(f'{name} references undeclared dependency: {ref[0]}')
if ref[0] == 'input':
if len(ref) < 2 or ref[1] not in inputs:
raise ValueError('Unknown workflow input')
used.add(ref[1])
for ref in references_in(contents.get('output', {})):
if ref[0] not in nodes:
raise ValueError('Unknown output node')
if used != set(inputs):View on GitHub (pinned to 8321021c54)