gastownhall/beads · error

applyGraph: edge %d %s->%s creates a blocking dependency cyc

Error message

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

What it means

During planned blocking-cycle validation, an edge whose resolved fromID equals its toID (a self-loop) is immediately rejected as a blocking dependency cycle — an issue cannot block itself. The message reports the edge index and the resolved issue ID on both sides.

Source

Thrown at internal/storage/domain/issue.go:1471

	}

	adj := make(map[string][]string)
	checks := make([]plannedEdge, 0, len(plan.Edges))
	for i, edge := range plan.Edges {
		depType := edge.Type
		if depType == "" {
			depType = types.DepBlocks
		}
		if !cycleRelevantDepType(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("applyGraph: 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 := u.graphHasPath(ctx, adj, depCache, edge.toID, edge.fromID, cycleRelevantDepType)
		if err != nil {
			return fmt.Errorf("applyGraph: edge %d %s->%s: checking planned blocking cycle: %w", edge.index, edge.fromID, edge.toID, err)
		}
		if hasPath {
			return fmt.Errorf("applyGraph: 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. Find edge <index> in the plan and check whether from and to resolve to the same node.
  2. Correct the edge so it connects two distinct issues, or delete it.
  3. Ensure node keys are unique and no aliasing collapses two keys to one ID.
  4. Re-run apply.

Example fix

// before: self edge
edges:
  - {from: bd-1, to: bd-1, type: blocks}
// after: distinct endpoints or removed
edges: []
Defensive patterns

Strategy: validation

Validate before calling

// Reject self-edges before apply
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", i)
    }
}

Type guard

func isSelfEdge(e Edge, keyToID map[string]string) bool {
    return resolveEdgeRef(e.FromKey, e.FromID, keyToID) != "" &&
        resolveEdgeRef(e.FromKey, e.FromID, keyToID) == resolveEdgeRef(e.ToKey, e.ToID, keyToID)
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
    if strings.Contains(err.Error(), "creates a blocking dependency cycle") {
        return fmt.Errorf("fix plan edges (self-loop or cycle): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: A plan edge where fromKey/fromID and toKey/toID resolve to the same issue — e.g. both set to the same node's key, duplicated keys, or resolveEdgeRef mapping two different key spellings to one ID.

Common situations: Copy-paste errors in plan files where from and to are identical; templated plan generation emitting the same key on both sides; aliasing where two keys map to one issue ID after creation.

Related errors


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