gastownhall/beads · error

edge %d %s->%s creates a blocking dependency cycle

Error message

edge %d %s->%s creates a blocking dependency cycle

What it means

Returned during `bd graph apply` plan validation when an edge in the plan has an identical FromID and ToID — a self-loop, which is the trivially blocking dependency cycle. The validator rejects it before touching the database because an issue cannot block itself.

Source

Thrown at cmd/bd/graph_apply.go:1134

		index  int
		fromID string
		toID   string
	}

	adj := make(map[string][]string)
	checks := make([]plannedEdge, 0, len(plan.Edges))
	for i, edge := range plan.Edges {
		depType := graphApplyDependencyType(edge.Type)
		if !graphApplyCycleRelevantDependencyType(depType) {
			continue
		}
		fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
		toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
		if fromID == "" || toID == "" {
			continue
		}
		if fromID == toID {
			return fmt.Errorf("edge %d %s->%s creates a blocking dependency cycle", i, fromID, toID)
		}
		adj[fromID] = append(adj[fromID], toID)
		checks = append(checks, plannedEdge{index: i, fromID: fromID, toID: toID})
	}

	depCache := make(map[string][]*types.Dependency)
	for _, edge := range checks {
		hasPath, err := graphApplyHasPath(ctx, tx, adj, depCache, edge.toID, edge.fromID, graphApplyCycleRelevantDependencyType)
		if err != nil {
			return fmt.Errorf("edge %d %s->%s: checking planned blocking cycle: %w", edge.index, edge.fromID, edge.toID, err)
		}
		if hasPath {
			return fmt.Errorf("edge %d %s->%s creates a blocking dependency cycle", edge.index, edge.fromID, edge.toID)
		}
	}
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the offending edge (index printed in the message) and change one endpoint to the intended different issue key
  2. Add a pre-submit check that skips or errors on edge.FromKey == edge.ToKey
  3. If both keys look different but resolve to the same ID, verify the keyToID mapping for duplicate/aliased keys

Example fix

// before
{"edges":[{"from":"bd-1","to":"bd-1"}]}
// after
{"edges":[{"from":"bd-1","to":"bd-2"}]}
Defensive patterns

Strategy: validation

Validate before calling

for i, e := range plan.Edges {
  if resolveEdgeRef(e.FromKey, e.FromID, keyToID) == resolveEdgeRef(e.ToKey, e.ToID, keyToID) {
    return fmt.Errorf("edge %d is a self-loop (%s)", i, e.FromKey)
  }
}

Prevention

When it happens

Trigger: Calling bd graph apply with a dependency edge whose FromKey/ToKey (or FromID/ToID) resolve to the same issue, e.g. JSON input where "from":"bd-1","to":"bd-1".

Common situations: Programmatic plan generation with an off-by-one or aliasing bug reusing the same variable for both endpoints; key-to-ID resolution collapsing two intended keys to the same ID; hand-edited JSON.

Related errors


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