gastownhall/beads · error
expansion depth limit exceeded: max %d levels (currently at
Error message
expansion depth limit exceeded: max %d levels (currently at %d) - step %q
What it means
expandStep tracks recursion depth; if depth exceeds DefaultMaxExpansionDepth (5), it aborts with this error. This guards against infinite/recursive expansions where templates reference themselves or expansions cascade unboundedly through nested children.
Source
Thrown at internal/formula/expand.go:198
// countStepIDs counts occurrences of each step ID recursively.
func countStepIDs(steps []*Step, counts map[string]int) {
for _, step := range steps {
counts[step.ID]++
if len(step.Children) > 0 {
countStepIDs(step.Children, counts)
}
}
}
// expandStep expands a target step using the given template.
// Returns the expanded steps with placeholders substituted.
// The depth parameter tracks recursion depth for children; if it exceeds
// DefaultMaxExpansionDepth, an error is returned.
// The vars parameter provides variable values for {varname} substitution.
func expandStep(target *Step, template []*Step, depth int, vars map[string]string) ([]*Step, error) {
if depth > DefaultMaxExpansionDepth {
return nil, fmt.Errorf("expansion depth limit exceeded: max %d levels (currently at %d) - step %q",
DefaultMaxExpansionDepth, depth, target.ID)
}
result := make([]*Step, 0, len(template))
for _, tmpl := range template {
expanded := &Step{
ID: substituteVars(substituteTargetPlaceholders(tmpl.ID, target), vars),
Title: substituteVars(substituteTargetPlaceholders(tmpl.Title, target), vars),
Description: substituteVars(substituteTargetPlaceholders(tmpl.Description, target), vars),
Type: tmpl.Type,
Priority: tmpl.Priority,
Assignee: substituteVars(tmpl.Assignee, vars),
SourceFormula: tmpl.SourceFormula, // Preserve source from template
SourceLocation: tmpl.SourceLocation, // Preserve source location
}
// Substitute placeholders in labelsView on GitHub (pinned to 71377f2769)
Solutions
- Break the recursion in the template so expansion is finite
- Make select patterns exclude generated steps (prefix-based patterns)
- Restructure deeply nested generations into explicit steps or raise depth intentionally via the library constant if truly needed
Example fix
// before: template expands steps that re-match the rule
// after: generate IDs that don't match the select pattern
steps:
- id: "done-${i}" # select: "gen-*" no longer matches Defensive patterns
Strategy: validation
Validate before calling
// static check: expansion templates must not reference steps matching their own select/expand rule
func templateSelfReferences(f *formula.Formula, pattern string) bool {
for _, s := range f.Template {
if matched, _ := path.Match(pattern, s.ID); matched { return true }
}
return false
} Try / catch
steps, err := formula.ApplyExpansions(steps, compose)
if err != nil && strings.Contains(err.Error(), "depth limit exceeded") {
return fmt.Errorf("recursive expansion detected (max %d): %w", 5, err)
} Prevention
- Never write templates whose generated IDs re-match the expansion rule's select pattern
- Name generated steps with a prefix excluded from select patterns
- Add unit tests (like TestExpandStepDepthLimit) for recursive formula combinations
When it happens
Trigger: Any expandStep call chain reaching depth 6+: recursive expansion templates, an expansion whose output is re-expanded by a map/inline rule, or chained expansions across formulas (callers include ApplyExpansions, MaterializeExpansion, applyInlineExpansionsRecursive).
Common situations: Self-referential formula (template expands to steps that match the same expansion rule); mutually recursive expansions; a bug in select patterns matching newly created steps.
Related errors
- expand %q: %w
- map %q -> %q: %w
- applying inline expansions: %w
- applying expansions: %w
- applying expansions to %q: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f536175722e29879.
Report an issue: GitHub.