siyuan-note/siyuan · error

a list-item cannot directly contain another list-item; to ne

Error message

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

What it means

Returned by CheckListItemNesting in kernel/treenode/blocktree.go when both the resolved parent and child blocks have Type "i" (list item). SiYuan's correct nested-list structure is ListItem > List > ListItem; a list item may not be a direct AST child of another list item. CheckListItemNesting is only called in the move scenario where both source and target types are known.

Source

Thrown at kernel/treenode/blocktree.go:565

	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
}

func SetBlockTreePath(tree *parse.Tree) {
	RemoveBlockTreesByRootID(tree.Box, tree.ID)
	IndexBlockTree(tree)
}

func RemoveBlockTreesByRootID(boxID, rootID string) {
	sqlStmt := "DELETE FROM blocktrees WHERE root_id = ?"
	_, err := execForBox(boxID, sqlStmt, rootID)
	if err != nil {
		logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
		return
	}
}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Create or locate the NodeList child under the outer list-item first, then move the inner list-item under that list.
  2. Use the editor's drag/drop (which builds the intermediate List) instead of a raw parentID move.
  3. If no list exists under the outer item, insert one (NodeList) before moving the inner item.

Example fix

// before
MoveBlock(parentID: outerListItemID, id: innerListItemID) // -> cannot directly contain

// after: nest via an intermediate NodeList
listID := createListUnderListItem(outerListItemID)
MoveBlock(parentID: listID, id: innerListItemID)
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check list-item-to-list-item moves before calling the move API.
parent := treenode.GetBlockTree(parentID)
child := treenode.GetBlockTree(childID)
if parent != nil && child != nil && parent.Type == "i" && child.Type == "i" {
    return fmt.Errorf("nest via an intermediate NodeList under list-item %s", parentID)
}
return nil

Prevention

When it happens

Trigger: A move-block operation whose target parent (parentID) is a list item and whose moved block is also a list item. Invoked from api/block_op.go, MCP tools/block.go, and cli/cmd/block.go move handlers.

Common situations: Dragging a list item into another list item expecting sub-nesting, without the intermediate List node; programmatic re-parenting of list items.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/259402b1f38ab57e. Report an issue: GitHub.