siyuan-note/siyuan · error

invalid block structure: %s [%s] cannot contain %s [%s]

Error message

invalid block structure: %s [%s] cannot contain %s [%s]

What it means

Returned by invalidBlockContainmentError in kernel/treenode/block_structure.go. It fires from ValidateBlockPlacement, ValidateBlockReplacement, and ValidateBlockSubtree when CanContainBlock(parent.Type, child.Type) is false — meaning the Lute AST rule parent.CanContain(childType) rejects the parent→child combination (e.g. a paragraph holding another block, or a container type that cannot hold this specific child type).

Source

Thrown at kernel/treenode/block_structure.go:99

		return invalidBlockNodeError(newNode)
	}

	parent := oldNode.Parent
	for nil != parent && !isContentBlock(parent) {
		parent = parent.Parent
	}
	if nil != parent && !CanContainBlock(parent.Type, newNode.Type) {
		return invalidBlockContainmentError(parent, newNode)
	}
	return ValidateBlockSubtree(newNode)
}

func isContentBlock(node *ast.Node) bool {
	return nil != node && node.IsBlock() && ast.NodeKramdownBlockIAL != node.Type
}

func invalidBlockContainmentError(parent, child *ast.Node) error {
	return fmt.Errorf("invalid block structure: %s [%s] cannot contain %s [%s]",
		parent.Type.String(), parent.ID, child.Type.String(), child.ID)
}

func invalidBlockNodeError(node *ast.Node) error {
	if nil == node {
		return fmt.Errorf("invalid block structure: block node is nil")
	}
	return fmt.Errorf("invalid block structure: %s [%s] is not a content block", node.Type.String(), node.ID)
}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Inspect the parent.Type and child.Type in the error message and consult CanContainBlock to find a legal parent type.
  2. Insert an intermediate container block (e.g. a list under a list-item) so the parent→child pair becomes valid.
  3. When moving, prefer previousID/nextID sibling placement which the transaction layer handles as InsertAfter/InsertBefore and skips this check.

Example fix

// before: paragraph set as child of another paragraph
parent := &ast.Node{Type: ast.NodeParagraph}
child := treenode.NewParagraph("")
parent.AppendChild(child)
err := treenode.ValidateBlockPlacement(child) // -> cannot contain

// after: use a document/container root, or sibling placement
root := &ast.Node{Type: ast.NodeDocument}
root.AppendChild(child)
err := treenode.ValidateBlockPlacement(child)
Defensive patterns

Strategy: validation

Validate before calling

// Validate containment before inserting using the same rule the validator applies.
func canPlace(parent, child *ast.Node) error {
    if parent == nil || !parent.IsBlock() || parent.Type == ast.NodeKramdownBlockIAL {
        return fmt.Errorf("parent is not a content block")
    }
    if !treenode.CanContainBlock(parent.Type, child.Type) {
        return fmt.Errorf("parent %s cannot contain child %s", parent.Type, child.Type)
    }
    return nil
}

if err := canPlace(parentNode, childNode); err != nil { return err }

Type guard

func isValidContentBlock(n *ast.Node) bool {
    return n != nil && n.IsBlock() && n.Type != ast.NodeKramdownBlockIAL
}

Prevention

When it happens

Trigger: Constructing or mutating a block subtree (insert, move, replace) where a child block type is placed under a parent block type that the Lute AST does not allow to contain it. Reached via treenode.ValidateBlockPlacement / ValidateBlockReplacement / ValidateBlockSubtree, which guard block-op code paths including the HTTP block API, MCP block tools, and CLI block commands.

Common situations: Programmatic block building in a plugin or agent that manually assembles ast.Node trees; moving a block under a parent whose type was checked against the blocktree but whose AST-level containment rule is stricter; replacing a block with a new node of an incompatible category.

Related errors


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