argoproj/argo-workflows · error

graph with cycle

Error message

graph with cycle

What it means

TopologicalSorting detects cycles with a Kahn-style queue: after seeding, the queue is pre-allocated to len(graph), and any nil slot means fewer nodes than expected reached zero indegree — i.e. some nodes form a cycle and can never be scheduled. The function returns this fixed error to report an unsatisfiable dependency graph.

Source

Thrown at util/sorting/topological_sorting.go:44

				return nil, fmt.Errorf("invalid dependency %s", dependency)
			}
			nextNodeMap[dependency] = append(nextNodeMap[dependency], node.NodeName)
		}
	}

	queue := make([]*TopologicalSortingNode, len(graph))
	head, tail := 0, 0
	for nodeName, priorNodeCount := range priorNodeCountMap {
		if priorNodeCount == 0 {
			queue[tail] = nodeNameMap[nodeName]
			tail++
		}
	}

	for head < len(queue) {
		curr := queue[head]
		if curr == nil {
			return nil, fmt.Errorf("graph with cycle")
		}
		for _, next := range nextNodeMap[curr.NodeName] {
			if priorNodeCountMap[next] > 0 {
				if priorNodeCountMap[next] == 1 {
					queue[tail] = nodeNameMap[next]
					tail++
				}
				priorNodeCountMap[next]--
			}
		}
		head++
	}

	return queue, nil
}

View on GitHub (pinned to 35bff19146)

Solutions

  1. Break the cycle: audit Dependencies and remove or invert one edge (e.g. A→B and B→A means dropping one direction).
  2. Check for self-dependencies — a node listing its own NodeName in Dependencies.
  3. Run a cycle-detection pass (DFS with a visiting set) on the graph before calling TopologicalSorting to produce a better message naming the offending nodes.
  4. If edges are derived from template definitions, fix the workflow spec's depends clauses; the sort input mirrors them.

Example fix

// before
nodes := []*sorting.TopologicalSortingNode{
    {NodeName: "A", Dependencies: []string{"B"}},
    {NodeName: "B", Dependencies: []string{"A"}}, // cycle
}
// after
nodes := []*sorting.TopologicalSortingNode{
    {NodeName: "A"},
    {NodeName: "B", Dependencies: []string{"A"}},
}
Defensive patterns

Strategy: validation

Validate before calling

func hasCycle(graph []*sorting.TopologicalSortingNode) bool {
    state := map[string]int{} // 0 unvisited, 1 visiting, 2 done
    var visit func(string) bool
    visit = func(n string) bool {
        switch state[n] {
        case 1: return true
        case 2: return false
        }
        state[n] = 1
        for _, node := range graph {
            if node.NodeName != n { continue }
            for _, d := range node.Dependencies {
                if visit(d) { return true }
            }
        }
        state[n] = 2
        return false
    }
    for _, n := range graph { if visit(n.NodeName) { return true } }
    return false
}

Try / catch

sorted, err := sorting.TopologicalSorting(graph)
if err != nil {
    if err.Error() == "graph with cycle" {
        cycle := findCycleNodes(graph) // report specifics to the user
        return fmt.Errorf("workflow dependency cycle involving: %v", cycle)
    }
    return err
}

Prevention

When it happens

Trigger: Passing a graph whose Dependencies form a cycle, e.g. A depends on B and B depends on A (or a self-dependency A→A), so no node in the cycle ever reaches indegree 0 and the queue fills with nils.

Common situations: DAG templates with circular depends/after references between tasks; synchronization edges added in both directions by accident; self-referencing node built from a name that includes itself; test fixtures GraphWithCycle/GraphWithCycle2 exercising this path.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/145957924da827dd. Report an issue: GitHub.