gastownhall/beads · error

applyGraph: node %q: planned blocking dependencies create a

Error message

applyGraph: node %q: planned blocking dependencies create a path from parent %q to child %q

What it means

applyGraph's planned-blocking-path validation detected that the plan's blocking dependencies create a ready path from a parent issue to its own child — meaning the parent could be considered unblocked/ready only after its child, inverting the intended hierarchy ordering. The apply rejects this node's parent/child structure combined with the planned blocking edges.

Source

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

		adj[fromID] = append(adj[fromID], toID)
	}

	depCache := make(map[string][]*types.Dependency)
	for _, node := range plan.Nodes {
		parentID := node.ParentID
		if node.ParentKey != "" {
			parentID = keyToID[node.ParentKey]
		}
		childID := keyToID[node.Key]
		if childID == "" || parentID == "" {
			continue
		}
		hasPath, err := u.graphHasPath(ctx, adj, depCache, parentID, childID, readyPathDepType)
		if err != nil {
			return err
		}
		if hasPath {
			return fmt.Errorf("applyGraph: node %q: planned blocking dependencies create a path from parent %q to child %q", node.Key, parentID, childID)
		}
	}
	return nil
}

// validatePlannedBlockingCycles rejects planned blocking edges that would close
// a blocking-dependency cycle, evaluated whole-graph before any insert. It
// mirrors embedded validateGraphApplyPlannedBlockingCycles. This early
// preflight is intentionally restricted to blocking edges; repository Insert
// subsequently enforces the combined scheduling graph for every stored edge.
func (u *issueUseCaseImpl) validatePlannedBlockingCycles(
	ctx context.Context,
	plan GraphPlan,
	keyToID map[string]string,
) error {
	type plannedEdge struct {
		index  int
		fromID string

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the message to identify the node and the parent/child IDs on the offending path.
  2. Remove or reverse the blocking edge that links parent back toward child.
  3. Restructure the plan so parents never depend (transitively) on their children.
  4. Re-run apply after the hierarchy and blocking edges are consistent.

Example fix

// before: parent blocked by its own child
nodes:
  - key: parent
    deps: [{target: child, type: blocks}]
  - key: child
    parent: parent
// after: child blocks on parent context, not the reverse
nodes:
  - key: parent
  - key: child
    parent: parent
    deps: [{target: parent, type: blocks}]
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check: no blocking path may go from any parent to its child
for _, n := range plan.Nodes {
    if n.Parent == "" { continue }
    if reachable(adj(plan.Edges, readyPathDepType), n.Key /*parent*/, n.Parent /*child*/) {
        return fmt.Errorf("node %s: blocking path parent->child", n.Key)
    }
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
    if strings.Contains(err.Error(), "path from parent") {
        return fmt.Errorf("restructure hierarchy: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: A node has a parent-child relationship (parentID -> childID) while the planned blocking edges (filtered to readyPathDepType) produce a directed path parent -> child, so the parent transitively depends on its own descendant.

Common situations: Adding a blocking edge from a parent to a subtask that the subtask's subtree blocks back; plans where parent/child roles were accidentally swapped; composing a subgraph plan onto an existing tree where cross edges close the path.

Related errors


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