siyuan-note/siyuan · error

block [ ] type is locked: expected , got

Error message

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

What it means

validateBlockUpdateType enforces the LockType option of the block update API: when lockType is true, the replacement block must keep the same node type as the original (empty paragraph blocks are exempt since they carry no meaningful type). If the type changes, the update is rejected with a message showing the block ID, expected type, and actual type. This prevents an update from silently converting e.g. a paragraph into a heading.

Solutions

  1. Set lockType=false (or omit it) if changing the block type is intended
  2. Keep the replacement payload's outermost block type identical to the existing block (e.g. wrap content in a paragraph node when updating a paragraph)
  3. Use delete + insert operations when the block type must change
  4. Match the message's expected/got types to adjust the payload accordingly

Example fix

// before
{"id": "...", "dataType": "markdown", "data": "# heading text", "lockType": true} // updating a paragraph
// after
{"id": "...", "dataType": "markdown", "data": "heading text", "lockType": true}
Defensive patterns

Strategy: validation

Validate before calling

// only send lockType=true when the outermost block type is unchanged
if (payload.lockType && outerBlockTypeOf(payload.data) !== currentBlockType) {
  payload.lockType = false; // or transform the payload
}

Type guard

const sameType = (oldT, newT) => oldT === newT || oldT === "NodeParagraph";

Try / catch

try {
  await updateBlock(payload);
} catch (e) {
  if (String(e.msg).includes("type is locked")) {
    // retry with lockType=false or fix the payload's block type
  }
}

Prevention

When it happens

Trigger: Calling buildBlockUpdateOperations / doUpdate (the updateBlock API) with LockType=true while the new payload parses to a different block type than the existing block — e.g. replacing a NodeParagraph with a NodeHeading, or a paragraph with a code block.

Common situations: Clients that pass lockType=true out of habit while sending structurally different content; plugins updating a block's content but accidentally changing its element type; automation converting block types when only text edit was intended.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: 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 9f775e8a12)