windmill-labs/windmill · error
Cycle detected: adding edge from '${sourceId}' to '${targetI
Error message
Cycle detected: adding edge from '${sourceId}' to '${targetId}' would create a cycle. What it means
addEdge runs a DFS cycle detection before inserting an edge; adding the requested source→target edge would make the flow graph cyclic, which React Flow / flow semantics do not support. The edge is rejected instead of silently creating a loop.
Source
Thrown at frontend/src/lib/components/graph/graphBuilder.svelte.ts:582
disableMoveIds?: string[]
}
) {
parents[targetId] = [...(parents[targetId] ?? []), sourceId]
let index: number
if (options?.currentItems) {
index = findInsertIndexByNodeId(options.currentItems, targetId)
} else {
const mods = options?.subModules ?? modules
const found = mods?.findIndex((m) => m.id === targetId) ?? -1
index = found >= 0 ? found : (mods?.length ?? 0)
}
const visited = new Set<string>()
const recStack = new Set<string>()
if (detectCycle(sourceId, visited, recStack)) {
throw new Error(
`Cycle detected: adding edge from '${sourceId}' to '${targetId}' would create a cycle.`
)
}
edges.push({
id: options?.customId || `edge:${sourceId}->${targetId}`,
source: sourceId,
target: targetId,
type: options?.type ?? 'edge',
data: {
sourceId,
targetId,
branch,
eventHandlers,
simplifiedTriggerView: simplifiableFlow?.simplifiedFlow,
disableMoveIds: options?.disableMoveIds,
enableTrigger: sourceId === 'Input',
index,View on GitHub (pinned to e474e8803c)
Solutions
- Remove the edge that loops back to an ancestor step; flows are DAGs.
- Restructure the logic with a loop/iterator construct (foreach step) instead of a graph cycle.
- If the edge is intentional only for visualization, give it a distinct handling path rather than pushing it as a real edge.
- Audit the code that generates edges so dependency order (topological) is respected.
Example fix
// before
addEdge('step3', 'step1') // creates cycle
// after
// model iteration with a foreach step containing step1..step3, no back edge Defensive patterns
Strategy: validation
Validate before calling
function wouldCreateCycle(edges, source, target) {
const adj = new Map(); edges.forEach(e => adj.set(e.source, [...(adj.get(e.source)||[]), e.target]))
const stack = [target]; const seen = new Set()
while (stack.length) { const n = stack.pop(); if (n === source) return true; if (seen.has(n)) continue; seen.add(n); stack.push(...(adj.get(n)||[])) }
return false
} Type guard
function isDagAddition(edges, source, target) { return !wouldCreateCycle(edges, source, target) } Try / catch
try { addEdge(sourceId, targetId) } catch (e) { if (String(e.message).startsWith('Cycle detected')) toast('Flows cannot contain loops; use a foreach step'); else throw e } Prevention
- Only add edges from earlier to later steps (topological order)
- Model iteration with foreach/loop constructs, not back edges
- Run cycle detection in UI before committing a drag-connect
When it happens
Trigger: Calling addEdge (directly or via processModules) where targetId is an ancestor of sourceId in the existing edge set — e.g. wiring a later step back to an earlier one, or branch child edges that loop back to the parent.
Common situations: Hand-editing flow edges to model loops/branches back-edges; a bug in custom flow generation that emits both forward and backward dependency edges; dragging an edge in the editor from a child step to an ancestor.
Related errors
- Group '${g.id}' has inverted range: start_id='${g.start_id}'
- Groups '${a.id}' and '${b.id}' overlap without nesting
- Duplicated node detected: ${module.id}
- end '${idLabel(d)}' is not downstream of the start — ignored
- end '${idLabel(e)}' is only reachable through a skipped inpu
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/54e699185d9c6b8d.
Report an issue: GitHub.