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) {
// 目标块不存在时忽略处理
returnView on GitHub (pinned to 251596fc0d)
Solutions
- 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.
- 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.
- 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
- Never use a notebook's top-level root document as the srcID for Doc2Heading.
- In the UI, disable drag-to-heading for notebook root documents.
- Verify the source document is a child document, not the box doc, before calling the API.
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
- Cannot be converted to heading when including sub-documents
- Encrypted notebooks do not support this operation
- encrypted notebook is locked, please unlock it first
- invalid box id
- encrypted notebook is locked, please unlock it first
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/55e7aa243f680684.
Report an issue: GitHub.