siyuan-note/siyuan · warning

block [%s] type is locked: expected %s, got %s

Error message

block [%s] type is locked: expected %s, got %s

What it means

Returned by validateBlockUpdateType. When lockType is true, the updated node's Type must equal the old node's Type (with an exemption for empty paragraph blocks via isEmptyParagraphBlock). Any other type transition is rejected to prevent silent block-type conversion during an update. The message names the old ID, the expected (old) type, and the actual (new) type.

Source

Thrown at kernel/model/block_update.go:316

}

func firstContentBlock(parent *ast.Node) *ast.Node {
	if nil == parent {
		return nil
	}
	for child := parent.FirstChild; nil != child; child = child.Next {
		if child.IsBlock() && ast.NodeKramdownBlockIAL != child.Type {
			return child
		}
	}
	return nil
}

func validateBlockUpdateType(oldNode, updatedNode *ast.Node, lockType bool) error {
	if !lockType || oldNode.Type == updatedNode.Type || isEmptyParagraphBlock(oldNode) {
		return nil
	}
	return fmt.Errorf("block [%s] type is locked: expected %s, got %s",
		oldNode.ID, oldNode.Type.String(), updatedNode.Type.String())
}

func isEmptyParagraphBlock(node *ast.Node) bool {
	if nil == node || ast.NodeParagraph != node.Type {
		return false
	}
	for child := node.FirstChild; nil != child; child = child.Next {
		switch child.Type {
		case ast.NodeText:
			text := strings.ReplaceAll(string(child.Tokens), "\u200b", "")
			if "" != strings.TrimSpace(text) {
				return false
			}
		case ast.NodeSoftBreak, ast.NodeBr:
		default:
			return false
		}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. When the user's intent is to change block type, use delete+insert (or a dedicated transform API) instead of BlockUpdate.
  2. Keep the submitted DOM's root content block type identical to the existing block when lockType is on.
  3. If the API exposes a lockType toggle and type change is intended, pass lockType=false.

Example fix

// before
// updating a paragraph with markdown "# Heading" -> type changes Paragraph -> Heading

// after
// delete the paragraph, then insert the heading as a separate operation
// or submit markdown that keeps the paragraph shape and use a separate transform path
Defensive patterns

Strategy: validation

Validate before calling

// determine parsed type of the new DOM and compare to old type
newTree := lute.BlockDOM2Tree(input.Data)
if firstContentBlock(newTree.Root).Type != oldNodeType && !isEmptyParagraph(oldNode) {
    return errors.New("type would change; use delete+insert instead")
}

Prevention

When it happens

Trigger: Updating a paragraph block with content that parses as a heading/list/blockquote; updating a heading with body text that Lute rewrites to a paragraph; submitting DOM whose root content block type differs from the target.

Common situations: User changes a paragraph into a heading in the editor and the integration sends it as an update rather than a replace; markdown input whose leading # changes the parsed type.

Related errors


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