gastownhall/beads · error
childRef is empty after variable substitution
Error message
childRef is empty after variable substitution
What it means
generateBondedID builds a bonded molecule child ID by substituting template variables into opts.ChildRef. If substitution produces an empty string (e.g. the childRef was entirely a variable like '{{name}}' and that variable resolved to empty), it throws 'childRef is empty after variable substitution'. The guard exists because an empty childRef would yield malformed IDs like 'parent.'.
Solutions
- Pass a non-empty value for the variable used in ChildRef in CloneOptions.Vars.
- Set a literal (non-variable) ChildRef if no dynamic naming is needed.
- Check the proto template for which variables it expects in childRef and provide them all.
- If a variable may be empty, give ChildRef a static prefix, e.g. 'task-{{n}}' instead of '{{n}}'.
Example fix
// before
opts := CloneOptions{ChildRef: "{{suffix}}", Vars: map[string]string{}}
// after
opts := CloneOptions{ChildRef: "{{suffix}}", Vars: map[string]string{"suffix": "step1"}} Defensive patterns
Strategy: validation
Validate before calling
resolved := substituteVariables(opts.ChildRef, opts.Vars)
if resolved == "" {
return fmt.Errorf("childRef resolves to empty; provide vars for %s", opts.ChildRef)
} Try / catch
newID, err := generateBondedID(ctx, tx, oldID, rootID, opts)
if err != nil && strings.Contains(err.Error(), "childRef is empty") {
return fmt.Errorf("provide a value for the childRef variable: %w", err)
} Prevention
- Ensure every variable used in ChildRef has a non-empty entry in CloneOptions.Vars.
- Prefer mixed literal+variable childRefs ('task-{{n}}') over pure variables.
- Validate Vars keys against the template's extractVariables output before cloning.
When it happens
Trigger: Cloning/bonding a subgraph (cloneSubgraphInto or direct call) where CloneOptions.ChildRef consists only of variables that resolve to empty strings in opts.Vars — e.g. ChildRef='{{suffix}}' with Vars lacking 'suffix' or having an empty value.
Common situations: Template variables renamed in the proto but the caller still passes old variable names in Vars; an empty CLI flag value substituted in; Handlebars-style {{var}} left unresolved and stripped to empty.
Related errors
- invalid childRef ' ': must be alphanumeric, dash…
- cannot modify template
- ErrVarValidation
- formula
- missing required variables
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6e24195f68dd7043.
Report an issue: GitHub.
Appendix: source
Thrown at cmd/bd/template.go:513
// generateBondedID creates a custom ID for dynamically bonded molecules.
// When bonding a proto to a parent molecule, this generates IDs like:
// - Root: parent.childref (e.g., "patrol-x7k.arm-ace")
// - Children: parent.childref.step (e.g., "patrol-x7k.arm-ace.capture")
//
// The childRef is variable-substituted before use.
// Returns empty string if not a bonded operation (opts.ParentID empty).
func generateBondedID(oldID string, rootID string, opts CloneOptions) (string, error) {
if opts.ParentID == "" {
return "", nil // Not a bonded operation
}
// Substitute variables in childRef
childRef := substituteVariables(opts.ChildRef, opts.Vars)
// Validate childRef after substitution
if childRef == "" {
return "", fmt.Errorf("childRef is empty after variable substitution")
}
if !bondedIDPattern.MatchString(childRef) {
return "", fmt.Errorf("invalid childRef '%s': must be alphanumeric, dash, underscore, or dot only", childRef)
}
if oldID == rootID {
// Root issue: parent.childref
newID := fmt.Sprintf("%s.%s", opts.ParentID, childRef)
return newID, nil
}
// Child issue: parent.childref.relative
// Extract the relative portion of the old ID (part after root)
relativeID := getRelativeID(oldID, rootID)
if relativeID == "" {
// No hierarchical relationship - use a suffix from the old ID to ensure uniqueness.
// Extract the last part of the old ID (after any prefix or dash)
suffix := extractIDSuffix(oldID)View on GitHub (pinned to 71377f2769)