siyuan-note/siyuan · error

block is not a list item

Error message

block is not a list item

What it means

Thrown by buildUpdatedTaskListItemBlockDOM (kernel/api/block_op.go) when the block loaded from the index has a Type other than "NodeListItem". The task-marker update endpoint only operates on list items; passing any other block type (paragraph, heading, code block, blockquote, embed...) is rejected before the tree is even loaded.

Source

Thrown at kernel/api/block_op.go:56

		util.BindJsonArg("dataType", &input.DataType, true, true),
		util.BindJsonArg("lockType", &input.LockType, false, false),
	) {
		return
	}
	if util.InvalidIDPattern(input.ID, ret) {
		return input, false
	}
	return input, true
}

func buildUpdatedTaskListItemBlockDOM(id, marker string, luteEngine *lute.Lute) (data string, err error) {
	block, err := model.GetBlock(id, nil)
	if err != nil {
		return "", errors.New("get block failed: " + err.Error())
	}

	if "NodeListItem" != block.Type {
		return "", errors.New("block is not a list item")
	}

	tree, err := filesys.LoadTree(block.Box, block.Path, luteEngine)
	if err != nil {
		return "", errors.New("load tree failed: " + err.Error())
	}

	li := treenode.GetNodeInTree(tree, id)
	if li == nil {
		return "", errors.New("block not found")
	}

	if 3 != li.ListData.Typ {
		return "", errors.New("block is not a task list item")
	}

	if 1 != len(marker) {
		return "", errors.New("task list item marker length should be 1")

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Fetch the block first (/api/block/getBlockInfo) and check its type equals NodeListItem before calling
  2. Target the specific list item block id, not its parent list or sibling content
  3. Filter candidate ids client-side so only NodeListItem blocks are sent
  4. If you need task semantics on another block type, convert it to a task list item in the editor first, then call

Example fix

// before
fetchPost('/api/block/updateTaskListItemMarker', { id: paragraphId, marker: 'x' }); // "block is not a list item"

// after
const info = await fetchPost('/api/block/getBlockInfo', { id });
if (info.data.type === 'NodeListItem') {
  fetchPost('/api/block/updateTaskListItemMarker', { id, marker: 'x' });
}
Defensive patterns

Strategy: type-guard

Validate before calling

const info = await fetchPost('/api/block/getBlockInfo', { id });
if (info.data?.type !== 'NodeListItem') {
  throw new Error(`block [${id}] is ${info.data?.type}, not a list item`);
}
await fetchPost('/api/block/updateTaskListItemMarker', { id, marker });

Type guard

function isListItemBlock(b: { type: string } | null | undefined): b is { type: 'NodeListItem' } {
  return b?.type === 'NodeListItem';
}

Try / catch

if (e.message === 'block is not a list item') { /* re-select the target: use the list item's own id, not a parent/sibling */ }

Prevention

When it happens

Trigger: POST /api/block/updateTaskListItemMarker with the id of a paragraph, heading, or any non-list block; passing a container block id (list itself) instead of the individual list item id; frontend gutter/checkbox handler wired to the wrong block after a DOM-to-id mapping mistake.

Common situations: A plugin toggles a checkbox it located by walking the DOM and grabs the parent paragraph; the block was converted from a list item to another type after the id was captured; batch updates iterate over a selection that includes non-list blocks.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/289580e315e1092e. Report an issue: GitHub.