argoproj/argo-workflows · error

dependency cycle detected: %s->%s

Error message

dependency cycle detected: %s->%s

What it means

validateNoCycles performs a DFS over the template dependency graph (an adjacency list of template/step names to their dependencies). It returns this error when following dependencies leads back to a node already on the current path, i.e. the graph contains a cycle, which would make scheduling impossible.

Source

Thrown at pkg/apis/workflow/v1alpha1/validation_utils.go:90

}

// validateNoCycles validates that a dependency graph has no cycles by doing a Depth-First Search
// depGraph is an adjacency list, where key is a node name and value is a list of its dependencies' names
func validateNoCycles(depGraph map[string][]string) error {
	visited := make(map[string]bool)
	var noCyclesHelper func(currentName string, cycyle []string) error
	noCyclesHelper = func(currentName string, cycle []string) error {
		if _, ok := visited[currentName]; ok {
			return nil
		}
		depNames, ok := depGraph[currentName]
		if !ok {
			return nil
		}
		for _, depName := range depNames {
			for _, name := range cycle {
				if depName == name {
					return fmt.Errorf("dependency cycle detected: %s->%s", strings.Join(cycle, "->"), name)
				}
			}
			cycle = append(cycle, depName)
			err := noCyclesHelper(depName, cycle)
			if err != nil {
				return err
			}
			cycle = cycle[0 : len(cycle)-1]
		}
		visited[currentName] = true
		return nil
	}
	names := make([]string, 0)
	for name := range depGraph {
		names = append(names, name)
	}
	// sort names here to make sure the error message has consistent ordering
	// so that we can verify the error message in unit tests

View on GitHub (pinned to 35bff19146)

Solutions

  1. Read the cycle from the error message (A->B->...->A) and remove or reverse one dependency edge
  2. Draw the DAG and check for the reported loop before adding new dependencies
  3. Refactor the cyclic group by splitting one template so the dependency is one-directional

Example fix

# before
A:
  dependencies: [B]
B:
  dependencies: [A]
# after
A: {}
B:
  dependencies: [A]
Defensive patterns

Strategy: validation

Validate before calling

func hasCycle(graph map[string][]string) bool {
    var visit func(string, []string) bool
    visit = func(n string, path []string) bool {
        for _, p := range path {
            if p == n { return true }
        }
        for _, d := range graph[n] {
            if visit(d, append(path, n)) { return true }
        }
        return false
    }
    for n := range graph {
        if visit(n, nil) { return true }
    }
    return false
}

Type guard

null

Try / catch

if err := wf.Validate(); err != nil {
    if strings.Contains(err.Error(), "dependency cycle detected") {
        cycle := strings.TrimPrefix(err.Error(), "dependency cycle detected: ")
        log.Printf("fix DAG cycle: %s", cycle)
    }
    return err
}

Prevention

When it happens

Trigger: Submitting or validating a Workflow whose templates' `dependencies` fields form a loop, e.g. A depends on B and B depends on A; triggered via Validate / DAG validation whenever noCyclesHelper finds a depName already present in the current cycle path.

Common situations: Hand-editing dependencies in a large DAG; auto-generated DAGs where an index off-by-one wraps around; adding a new edge that accidentally closes a loop.

Related errors


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