siyuan-note/siyuan · error

heading [ ] is a leaf block and cannot have children; to…

Error message

heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID

What it means

In SiYuan's data model a heading is a leaf block — content that visually appears 'under' a heading is actually stored as subsequent sibling nodes (computed by HeadingChildren), not AST children. CheckContainerParent rejects using a heading as parentID and instructs the caller to position with previousID instead. The long message documents the exact remedy.

Solutions

  1. Pass previousID = the heading ID (new block becomes the sibling right below the heading) instead of parentID
  2. To append after all content under the heading, compute the last block below it (HeadingChildren semantics) and use that as previousID
  3. If you truly need nesting, use a container type (super block, list item, blockquote) rather than a heading

Example fix

// before
await insertBlock({ parentID: headingID, data }) // rejected: heading is a leaf
// after
await insertBlock({ previousID: headingID, data }) // sibling below the heading
Defensive patterns

Strategy: validation

Validate before calling

const parent = await getBlockInfo(parentID);
if (parent && parent.type === "h") {
  options.previousID = parentID; delete options.parentID; // heading is a leaf: switch to sibling placement
}

Type guard

function isHeading(info) { return info?.type === "h"; }

Prevention

When it happens

Trigger: blockInsert/blockAppend with parentID = a heading ID; validateBlockMove targeting a heading as the new parent; plugins that assume headings behave like containers and pass heading IDs as parents.

Common situations: Outliner-style plugins inserting notes 'under' headings; scripts migrating outlines that treat headings as folders; move operations whose drop target was computed as a heading container.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/0c35c8af9cbdd6dd. Report an issue: GitHub.

Appendix: source

Thrown at kernel/treenode/blocktree.go:528

	return false
}

// CheckContainerParent 校验 parentID 指向的块是否允许接收子块。
// 仅在“通过 parentID 定位插入/移动目标”(即不依赖 previousID/nextID)的场景下调用,
// 因为一旦带 previousID/nextID,事务层走的是兄弟级 InsertAfter/InsertBefore,天然合法。
// 返回 nil 表示合法;返回 error 时调用方应拒绝本次操作。
func CheckContainerParent(parentID string) error {
	bt := GetBlockTree(parentID)
	if nil == bt {
		return fmt.Errorf("parent block not found: %s", parentID)
	}
	if IsContainerType(bt.Type) {
		return nil
	}
	if "h" == bt.Type {
		// 标题是叶子块,其“子内容”在数据结构上实为后续兄弟节点(由 HeadingChildren 按层级推算)。
		// 把块挂成标题的 AST 子节点属于非法嵌套,应改用 previousID 定位。
		return fmt.Errorf("heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID", parentID)
	}
	return fmt.Errorf("block [%s] type %q is a leaf block and cannot have children; use previousID to place the block as its sibling instead", parentID, bt.Type)
}

// CheckListItemNesting 校验 parentID 和 childID 是否形成“列表项直含列表项”的非法嵌套。
// 嵌套列表的正确结构是 ListItem > List > ListItem,列表项不能直接作为另一个列表项的子块。
// 仅在 move 场景调用(源和目标类型均已知)。
func CheckListItemNesting(parentID, childID string) error {
	parentBt := GetBlockTree(parentID)
	childBt := GetBlockTree(childID)
	if nil == parentBt || nil == childBt {
		return nil // 查不到就放行,不阻塞未知场景
	}
	if "i" == parentBt.Type && "i" == childBt.Type {
		return fmt.Errorf("a list-item cannot directly contain another list-item; to nest, first create a list (NodeList) under the outer list-item, then add the inner list-items to that list")
	}
	return nil
}

View on GitHub (pinned to 9f775e8a12)