semaphoreui/semaphore · error
cyclic dependency detected involving
Error message
cyclic dependency detected involving %s
What it means
The export chain validates the dependency order of exporter types as a directed graph before exporting. If the dependency graph contains a cycle, visiting() detects a node that is currently on the visit stack and aborts with the name of one type involved in the cycle.
Solutions
- Break the cycle by removing or reordering the dependency declaration between the implicated types
- Restructure the custom exporter so dependencies flow one way (e.g. split into two passes)
- Trace dependencies starting from the type named in the message to find the loop edge
Example fix
// before: project depends on template, template depends on project
exporter.addDependency("template") // in project exporter
exporter.addDependency("project") // in template exporter -> cycle
// after: remove the back-edge
// template no longer declares a dependency on project Defensive patterns
Strategy: try-catch
Validate before calling
// detect cycle before running export
deps := map[string][]string{"a": {"b"}, "b": {"a"}}
var hasCycle func(n string, stack map[string]bool) bool
hasCycle = func(n string, stack map[string]bool) bool {
if stack[n] { return true }
stack[n] = true
for _, d := range deps[n] { if hasCycle(d, stack) { return true } }
stack[n] = false
return false
} Try / catch
if err := chain.export(); err != nil {
if strings.Contains(err.Error(), "cyclic dependency") {
log.Fatalf("fix exporter dependency declarations: %v", err)
}
return err
} Prevention
- Keep exporter dependencies strictly one-directional
- Run a topological-sort unit test on the chain at startup
- Avoid mutual references between custom exporters
When it happens
Trigger: Registering exporters whose ExportOrder/dependency declarations form a cycle (A depends on B, B on A — possibly through longer chains), then running the topological sort during export/import.
Common situations: Adding a new custom exporter that declares a dependency on a type which (transitively) depends back on it; refactoring dependency lists so two entity types mutually reference each other.
Related errors
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/14fd13a5d646c2dd.
Report an issue: GitHub.
Appendix: source
Thrown at services/export/Exporter.go:363
n, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
out[i] = n
}
return out, nil
}
func getSortedKeys(exporters map[string]TypeExporter, dependsOn func(t TypeExporter) []string) ([]string, error) {
var sorted []string
visited := make(map[string]bool)
visiting := make(map[string]bool)
var visit func(name string) error
visit = func(name string) error {
if visiting[name] {
return fmt.Errorf("cyclic dependency detected involving %s", name)
}
if visited[name] {
return nil
}
visiting[name] = true
if exporter, ok := exporters[name]; ok {
order := dependsOn(exporter)
for _, dep := range order {
if _, exists := exporters[dep]; exists {
if err := visit(dep); err != nil {
return err
}
}
}View on GitHub (pinned to 1774ccb71a)