siyuan-note/siyuan · error

document [ ] cannot be pinned

Error message

document [%s] cannot be pinned

What it means

isPinnableDocument only accepts documents that are notebook roots or regular top-level documents: the block-tree record must exist, tree.ID == tree.RootID, tree.Type == "d", and the notebook must not be encrypted. If the ID resolves to a non-document block, a child document without an allowed tree shape, or an unknown/unindexed block (bt == nil), the pin is rejected with this error.

Solutions

  1. Pass the document root ID, not a child block ID.
  2. Wait for indexing to finish (or trigger a reindex) so GetBlockTree can resolve the ID.
  3. Remove the custom-doc-hidden attribute or unhide the document before pinning.
  4. Verify the document still exists and was not replaced by a new ID.

Example fix

// before
model.UpdatePinnedDocs([]string{blockID}, "pin", "", false)
// after
bt := treenode.GetBlockTree(blockID)
if bt == nil || bt.ID != bt.RootID || bt.Type != "d" {
    return fmt.Errorf("%s is not a pinnable document", blockID)
}
model.UpdatePinnedDocs([]string{blockID}, "pin", "", false)
Defensive patterns

Strategy: validation

Validate before calling

func pinnable(id string) bool {
    bt := treenode.GetBlockTree(id)
    return bt != nil && bt.ID == bt.RootID && bt.Type == "d" && !model.IsEncryptedBox(bt.BoxID)
}

Type guard

func isDocRoot(bt *treenode.BlockTree) bool { return bt != nil && bt.ID == bt.RootID && bt.Type == "d" }

Try / catch

if err := model.UpdatePinnedDocs(ids, "pin", "", false); err != nil {
    if strings.Contains(err.Error(), "cannot be pinned") {
        // re-resolve IDs, drop non-document roots, retry
    }
}

Prevention

When it happens

Trigger: Pinning an ID that resolves to a block tree entry where isPinnableDocument returns false: a non-root block ID (heading, paragraph), a bt == nil (not in block tree), or a doc that is hidden while also being the notebook root ID form (see the DocHiddenAttr check which reuses this message). Also triggered when the id equals bt.BoxID but ial[DocHiddenAttr]=="true" at line 200.

Common situations: Plugin passes a paragraph or heading block ID instead of a document ID; block tree index is not yet built after import/sync so GetBlockTree returns nil; user tries to pin a hidden document; stale ID after the document was deleted and re-created.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/pinned_docs.go:189

	selected := map[string]bool{}
	refs := []pinnedDocRef{}
	for _, id := range ids {
		if !ast.IsNodeIDPattern(id) {
			return fmt.Errorf("invalid document ID [%s]", id)
		}
		if selected[id] {
			continue
		}
		selected[id] = true
		if action == "unpin" {
			continue
		}
		bt := treenode.GetBlockTree(id)
		if bt != nil && IsEncryptedBox(bt.BoxID) {
			return fmt.Errorf("%s", Conf.Language(396))
		}
		if !isPinnableDocument(bt) {
			return fmt.Errorf("document [%s] cannot be pinned", id)
		}
		box := Conf.Box(bt.BoxID)
		if box == nil || box.Stat(bt.Path) == nil {
			return fmt.Errorf("document [%s] is unavailable", id)
		}
		ial := box.docIAL(bt.Path)
		if ial == nil {
			return fmt.Errorf("cannot read pinned document [%s]", id)
		}
		if id != bt.BoxID && ial[DocHiddenAttr] == "true" {
			return fmt.Errorf("document [%s] cannot be pinned", id)
		}
		refs = append(refs, pinnedDocRef{ID: id, Notebook: bt.BoxID})
	}
	if action == "pin" && selected[targetID] {
		return nil
	}
	remaining := []pinnedDocRef{}

View on GitHub (pinned to 9f775e8a12)