elsa-workflows/elsa-core · error · InvalidOperationException
Circular dependency detected involving task type
Error message
Circular dependency detected involving task type: {node.Name} What it means
TopologicalTaskSorter orders task types by their dependencies using DFS. Visit tracks nodes on the current recursion stack in 'visiting'; encountering a node already on the stack means a dependency cycle, so it throws this InvalidOperationException naming the task type where the cycle closed.
Solutions
- Inspect the named task type's dependency registration and remove the entry that closes the cycle.
- Break the cycle by extracting the shared work into a third task both depend on.
- Log the full visiting stack at the throw point to see the whole cycle path.
- Validate the dependency graph at startup in a dev-only check before sorting.
Example fix
// before A.DependsOn = [B]; B.DependsOn = [A]; // after A.DependsOn = [C]; B.DependsOn = [C]; // C has no dependencies
Defensive patterns
Strategy: try-catch
Validate before calling
bool HasCycle(IEnumerable<(Type Task, Type[] DependsOn)> edges)
{
var graph = edges.ToDictionary(e => e.Task, e => e.DependsOn.ToList());
var state = new Dictionary<Type, int>(); // 0=unvisited 1=visiting 2=done
bool Dfs(Type n) {
if (state.TryGetValue(n, out var s)) return s == 1;
state[n] = 1;
foreach (var d in graph.GetValueOrDefault(n, new List<Type>())) if (Dfs(d)) return true;
state[n] = 2; return false;
}
return graph.Keys.Any(Dfs);
} Try / catch
try { var ordered = sorter.TopologicalSort(tasks); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Circular dependency detected")) { logger.LogCritical(ex, "Fix task dependency registration"); throw; } Prevention
- Keep dependency declarations one-directional per feature
- Add a startup self-check that sorts the graph before serving traffic
- Review DependsOn/StartTask attributes in code review for mutual references
When it happens
Trigger: Calling TopologicalSort (directly or via Visit) with a graph where task type A depends (transitively) on itself — e.g. feature A starts after B and B starts after A, or a task lists itself as a dependency.
Common situations: Registering features/tasks with mutually referencing StartTask/DependsOn attributes; a copy-paste duplicated dependency entry; two modules each claiming the other must start first after a version upgrade.
Related errors
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/67af7e5a4e89cee7.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Common/Helpers/TopologicalTaskSorter.cs:74
private static List<Type> TopologicalSort(Dictionary<Type, List<Type>> graph)
{
var sorted = new List<Type>();
var visited = new HashSet<Type>();
var visiting = new HashSet<Type>();
foreach (var node in graph.Keys)
{
if (!visited.Contains(node))
Visit(node, graph, visited, visiting, sorted);
}
return sorted;
}
private static void Visit(Type node, Dictionary<Type, List<Type>> graph, HashSet<Type> visited, HashSet<Type> visiting, List<Type> sorted)
{
if (visiting.Contains(node))
throw new InvalidOperationException($"Circular dependency detected involving task type: {node.Name}");
if (visited.Contains(node))
return;
visiting.Add(node);
if (graph.TryGetValue(node, out var dependencies))
{
foreach (var dependency in dependencies)
{
Visit(dependency, graph, visited, visiting, sorted);
}
}
visiting.Remove(node);
visited.Add(node);
sorted.Add(node);
}View on GitHub (pinned to fe9217bdfa)