siyuan-note/siyuan · error

The top-level notebook document cannot be removed or moved

Error message

The top-level notebook document cannot be removed or moved

What it means

Thrown by Doc2Heading when the source document (srcTree) is a box-level document — i.e., the top-level root document of a notebook. IsBoxDoc(srcTree.Box, srcTree.ID) returns true when srcTree.ID matches the notebook's (srcTree.Box) designated box document ID. Converting a notebook's top-level document into a heading inside another document would remove the notebook's root entry point, so it is explicitly forbidden. The message is Conf.Language(341) = "The top-level notebook document cannot be removed or moved".

Source

Thrown at kernel/model/heading.go:188

		}()
	}
	return
}

func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath string, err error) {
	if !ast.IsNodeIDPattern(srcID) || !ast.IsNodeIDPattern(targetID) {
		return
	}

	FlushTxQueue()

	srcTree, _ := LoadTreeByBlockID(srcID)
	if nil == srcTree {
		err = ErrBlockNotFound
		return
	}
	if IsBoxDoc(srcTree.Box, srcTree.ID) {
		err = errors.New(Conf.Language(341))
		return
	}

	subDir := filepath.Join(util.DataDir, srcTree.Box, strings.TrimSuffix(srcTree.Path, ".sy"))
	if gulu.File.IsDir(subDir) {
		if !util.IsEmptyDir(subDir) {
			err = errors.New(Conf.Language(20))
			return
		}

		if removeErr := os.Remove(subDir); nil != removeErr { // 移除空文件夹不会有副作用
			logging.LogWarnf("remove empty dir [%s] failed: %s", subDir, removeErr)
		}
	}

	if nil == treenode.GetBlockTree(targetID) {
		// 目标块不存在时忽略处理
		return

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Select a child document (not the notebook's top-level root document) as the source for Doc2Heading — the box document's position is fixed by design.
  2. If you need to move content out of a notebook root document, copy the relevant blocks manually into the target document instead of using Doc2Heading.
  3. As an API client, verify srcID is not the box document by checking IsBoxDoc before calling Doc2Heading.

Example fix

// before
await post('/api/filetree/doc2Heading', { srcID: notebookRootDocId, targetID, after: true })

// after — use a child document, never the notebook's top-level root doc
await post('/api/filetree/doc2Heading', { srcID: childDocId, targetID, after: true })
Defensive patterns

Strategy: validation

Validate before calling

// Verify srcID is not a box-level document before calling Doc2Heading
function isBoxDocument(boxID, docID) {
  // Check against the notebook's root document ID from configuration
  const notebook = notebooks.find(nb => nb.id === boxID)
  return notebook && notebook.rootDocID === docID
}
if (!isBoxDocument(srcBox, srcID)) {
  await post('/api/filetree/doc2Heading', { srcID, targetID, after })
}

Prevention

When it happens

Trigger: Calling POST /api/filetree/doc2Heading with a srcID that is the root document of a notebook (the .sy file directly under the notebook directory that serves as the notebook's main document). This typically happens when a user tries to drag the notebook's top-level document into another document in the file tree.

Common situations: User drags the notebook's root document into another document expecting it to merge as headings; an API client selects the box document ID as a source for conversion; confusion about which document is the notebook root vs. a child document.

Related errors


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