gastownhall/beads · error

graph dependency cycle would be created: %s

Error message

graph dependency cycle would be created: %s

What it means

This is the deliberate rejection raised when the final cycle check finds that the newly added scheduling dependencies would form a cycle. The message includes cyclePath, the concrete chain of issues forming the loop, so the developer can see exactly which edges to break.

Source

Thrown at cmd/bd/graph_apply.go:1088

						continue
					}
					d, err := types.NewGraphNodeDependency(issues[i].ID, depType, dep.Target, keyToID)
					if err != nil {
						return fmt.Errorf("node %q: %w", node.Key, err)
					}
					if err := tx.AddDependency(ctx, d, actor); err != nil {
						return fmt.Errorf("node %q: adding dep to %q: %w", node.Key, dep.Target, err)
					}
					if graphApplySchedulingDependencyType(d.Type) {
						newSchedulingEdges = append(newSchedulingEdges, [2]string{d.IssueID, d.DependsOnID})
					}
				}
			}
		}
		if cyclePath, err := tx.CycleThroughEdges(ctx, newSchedulingEdges); err != nil {
			return fmt.Errorf("final graph cycle check: %w", err)
		} else if cyclePath != "" {
			return fmt.Errorf("graph dependency cycle would be created: %s", cyclePath)
		}

		// Apply deferred assignees.
		for i, assignee := range pendingAssignees {
			updates := map[string]interface{}{
				"assignee": assignee,
			}
			if err := tx.UpdateIssue(ctx, issues[i].ID, updates, actor); err != nil {
				return fmt.Errorf("node %q: setting assignee: %w", plan.Nodes[i].Key, err)
			}
		}

		return nil
	}); err != nil {
		return nil, err
	}

	return &GraphApplyResult{IDs: keyToID}, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read cyclePath in the message to identify the loop's members
  2. Remove or reverse one edge in the reported path
  3. Change one edge to a non-scheduling type (not blocks) if a soft relation was intended
  4. Run bd blocked / bd dep tree on the path members to understand pre-existing deps

Example fix

// before
{"from":"a","to":"b","type":"blocks"},{"from":"b","to":"a","type":"blocks"}
// after: drop the back edge
{"from":"a","to":"b","type":"blocks"}
Defensive patterns

Strategy: validation

Validate before calling

// topological check over plan edges plus existing DB deps
if err := topoSort(planNodes, append(planEdges, existingDBDeps()...)); err != nil {
  return fmt.Errorf("plan would create a cycle: %w", err)
}

Type guard

func acyclic(edges []Edge) bool { return topoSort(nodeKeys(edges), edges) == nil }

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "cycle would be created") {
    path := extractCyclePath(err.Error())
    plan = dropOneEdgeOnPath(plan, path)
    return bd.GraphApply(ctx, plan)
  }
  return err
}

Prevention

When it happens

Trigger: tx.CycleThroughEdges returns a non-empty cyclePath after the plan's parent-child and scheduling edges are inserted — i.e. some combination of new edges closes a loop (A blocks B blocks ... blocks A).

Common situations: Hand-authored graphs where mutual blocks were specified, or a new edge that closes a loop through pre-existing dependencies in the database.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/270f2bf236d77c30. Report an issue: GitHub.