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

  1. Pass a non-empty value for the variable used in ChildRef in CloneOptions.Vars.
  2. Set a literal (non-variable) ChildRef if no dynamic naming is needed.
  3. Check the proto template for which variables it expects in childRef and provide them all.
  4. 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

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


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)