gastownhall/beads · error

edge -> 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.

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.

Appendix: 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)