siyuan-note/siyuan · error

recursive template call [%s] is not supported

Error message

recursive template call [%s] is not supported

What it means

validateTemplateCallGraph walks the static {{template ...}} call graph of a parsed template set and rejects any cycle: if visit() reaches a template definition that is already on the current visiting stack, the set is recursive and rendering is refused with this error. Go's text/template would infinitely recurse at execution time, so SiYuan detects it statically before executing.

Source

Thrown at kernel/model/template_doc_tree_render.go:112

	if nil != err {
		return nil, fmt.Errorf(Conf.Language(44), err.Error())
	}
	if err = validateTemplateCallGraph(childTemplate, childTemplate.Name()); nil != err {
		return nil, err
	}
	if err = childTemplate.Execute(buf, dataModel); nil != err {
		return nil, fmt.Errorf(Conf.Language(44), err.Error())
	}
	return buf.Bytes(), nil
}

func validateTemplateCallGraph(root *template.Template, start string) error {
	visiting := map[string]bool{}
	depths := map[string]int{}
	var visit func(string) (int, error)
	visit = func(name string) (int, error) {
		if visiting[name] {
			return 0, fmt.Errorf("recursive template call [%s] is not supported", name)
		}
		if depth, ok := depths[name]; ok {
			return depth, nil
		}
		tmpl := root.Lookup(name)
		if nil == tmpl || nil == tmpl.Tree || nil == tmpl.Tree.Root {
			return 0, fmt.Errorf("template definition [%s] not found", name)
		}
		visiting[name] = true
		depth := 1
		for _, called := range collectCalledTemplateNames(tmpl.Tree.Root) {
			calledDepth, err := visit(called)
			if nil != err {
				return 0, err
			}
			if depth < calledDepth+1 {
				depth = calledDepth + 1
			}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Break the recursion: remove the {{template}} call that closes the cycle or gate it behind a condition that cannot recurse
  2. Restructure shared content into a leaf define that both templates call instead of calling each other
  3. Duplicate the needed content into each template if mutual inclusion is truly required
  4. Trace the cycle by following {{template "..."}} calls from the named template in the error

Example fix

// before: mutual recursion
{{define "a"}}{{template "b"}}{{end}}
{{define "b"}}{{template "a"}}{{end}}
// after: extract shared leaf
{{define "a"}}{{template "leaf"}}{{end}}
{{define "b"}}{{template "leaf"}}{{end}}
{{define "leaf"}}shared content{{end}}
Defensive patterns

Strategy: validation

Validate before calling

func checkNoCycles(defines map[string][]string) error {
	visiting := map[string]bool{}
	var visit func(string) error
	visit = func(n string) error {
		if visiting[n] { return fmt.Errorf("cycle at %s", n) }
		visiting[n] = true
		for _, c := range defines[n] { if err := visit(c); err != nil { return err } }
		visiting[n] = false
		return nil
	}
	for n := range defines { if err := visit(n); err != nil { return err } }
	return nil
}

Prevention

When it happens

Trigger: A define (or external child template) contains {{template "a"}} where a eventually calls back into itself, directly or through a chain of defines, e.g. define A calls B and B calls A.

Common situations: Refactoring shared headers/footers into defines and accidentally re-including a parent; copy-pasting template includes creating mutual recursion between two child files; generics-style 'wrapper' patterns that don't terminate.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/438e45c3ee237333. Report an issue: GitHub.