gastownhall/beads · error

applyGraph: dependency cycle would be created: %s

Error message

applyGraph: dependency cycle would be created: %s

What it means

The final cycle check found that the edges in this plan would create a dependency cycle among scheduling edges; applyGraph refuses to apply and returns the cycle path in the message. This is an intentional validation failure, not an internal error — the plan itself is cyclic.

Source

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

				if err != nil {
					return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: %w", node.Key, err)
				}
				if (dep.Type == types.DepParentChild) != parentPhase {
					continue
				}
				if err := u.depRepo.Insert(ctx, dep, actor, DepInsertOpts{UseWispsTable: useWisp}); err != nil {
					return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: adding dep to %q: %w", node.Key, nd.Target, err)
				}
				if types.IsSchedulingEdge(dep.Type) {
					newSchedulingEdges = append(newSchedulingEdges, [2]string{dep.IssueID, dep.DependsOnID})
				}
			}
		}
	}
	if cyclePath, err := u.depRepo.CycleThroughEdges(ctx, newSchedulingEdges); err != nil {
		return GraphApplyResult{}, fmt.Errorf("applyGraph: final cycle check: %w", err)
	} else if cyclePath != "" {
		return GraphApplyResult{}, fmt.Errorf("applyGraph: dependency cycle would be created: %s", cyclePath)
	}

	// Pass 5 — apply deferred assignees.
	for i, assignee := range pendingAssignees {
		if assignee == "" {
			continue
		}
		id := keyToID[plan.Nodes[i].Key]
		if err := u.issueRepo.Update(ctx, id, map[string]any{"assignee": assignee}, actor, IssueTableOpts{UseWispsTable: useWisp}); err != nil {
			return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: defer assignee: %w", plan.Nodes[i].Key, err)
		}
	}

	return GraphApplyResult{IDs: keyToID}, nil
}

// graphParentDepPairs encodes the (childID, parentID) parent-child pairs
// implied by the plan's node ParentKey/ParentID fields. Used by applyGraph

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the cycle path printed in the message to identify the offending edge loop.
  2. Remove or reverse one edge in the cycle in the plan file.
  3. Check existing DB dependencies — the cycle may need only one new edge removed to break.
  4. Re-run bd graph apply after breaking the loop.

Example fix

// before: cyclic plan
edges:
  - {from: a, to: b, type: blocks}
  - {from: b, to: c, type: blocks}
  - {from: c, to: a, type: blocks}
// after: break the cycle by dropping/reversing one edge
edges:
  - {from: a, to: b, type: blocks}
  - {from: b, to: c, type: blocks}
Defensive patterns

Strategy: validation

Validate before calling

// Detect scheduling-edge cycles in the plan before apply
func hasSchedulingCycle(edges []Edge) bool {
    adj := map[string][]string{}
    for _, e := range edges { adj[e.From] = append(adj[e.From], e.To) }
    return detectCycleDFS(adj) != ""
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
    if cycle, ok := parseCyclePath(err); ok {
        fmt.Printf("remove one edge from this loop: %s\n", cycle)
        return errPlanCyclic
    }
    return err
}

Prevention

When it happens

Trigger: A plan whose blocking/scheduling edges form a loop, e.g. A blocks B, B blocks C, C blocks A (possibly combined with pre-existing edges in the DB closing a loop that only becomes cyclic with the new edges).

Common situations: Hand-authored or generated plans where blocking direction was inverted; merging plans from multiple sources that each look acyclic but compose into a cycle; an existing dependency in the DB plus one new edge closing the loop.

Related errors


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