siyuan-note/siyuan · error

template document tree plan contains an invalid parent undo

Error message

template document tree plan contains an invalid parent undo operation

What it means

validateTemplateDocTreeParentOperations pre-checks the transaction before a plan is attached. Every UndoOperation must be non-nil and carry a non-empty ID. This error means an undo operation is nil or missing its block ID, so it cannot be paired with a forward operation for reversibility.

Source

Thrown at kernel/model/template_doc_tree.go:539

			templateDocTreeRootID: plan.rootID,
		})
	}
	return true, nil
}

func validateTemplateDocTreeParentOperations(transaction *Transaction) error {
	inverseActions := map[string]string{
		"insert":        "delete",
		"delete":        "insert",
		"update":        "update",
		"foldHeading":   "unfoldHeading",
		"unfoldHeading": "foldHeading",
		"setAttrs":      "setAttrs",
	}
	undoOperations := map[string]int{}
	for _, operation := range transaction.UndoOperations {
		if nil == operation || "" == operation.ID {
			return errors.New("template document tree plan contains an invalid parent undo operation")
		}
		if _, supported := inverseActions[operation.Action]; !supported || "" != operation.RootID {
			return errors.New("template document tree plan contains an unsupported parent undo operation")
		}
		undoOperations[operation.Action+"\x00"+operation.ID]++
	}

	hasContentMutation := false
	for _, operation := range transaction.DoOperations {
		if nil == operation || "" == operation.ID {
			return errors.New("template document tree plan contains an invalid parent operation")
		}
		inverseAction, supported := inverseActions[operation.Action]
		if !supported || "" != operation.RootID {
			return errors.New("template document tree plan contains an unsupported parent operation")
		}
		if "insert" == operation.Action || "delete" == operation.Action || "update" == operation.Action {
			hasContentMutation = true

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Populate Operation.ID for every entry in UndoOperations (the block ID being reverted)
  2. Remove nil entries from the UndoOperations slice before calling performTransactions
  3. Build the transaction via the kernel's normal operation pipeline so IDs are assigned consistently
  4. Validate the transaction client-side (non-nil ops, non-empty IDs) before submission

Example fix

// before
undoOps := []*Operation{{Action: "delete"}} // ID missing
// after
undoOps := []*Operation{{Action: "delete", ID: parentBlockID}}
Defensive patterns

Strategy: validation

Validate before calling

for i, op := range tx.UndoOperations { if op == nil || op.ID == "" { return fmt.Errorf("undo operation %d missing ID", i) } }

Type guard

func validUndoOps(ops []*Operation) bool { for _, op := range ops { if op == nil || op.ID == "" { return false } }; return len(ops) > 0 }

Try / catch

if err != nil && strings.Contains(err.Error(), "invalid parent undo operation") { fix the transaction builder to assign IDs, then resubmit }

Prevention

When it happens

Trigger: Constructing a Transaction for a template plan whose UndoOperations slice contains a nil entry or an Operation with ID ""; hand-assembled transactions in scripts/tests omitting IDs; upstream code that builds undo ops without setting IDs.

Common situations: Plugin or integration code manually building plan transactions instead of using the standard transaction builder; a serialization bug dropping empty IDs; tests constructing minimal fixtures.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/df4635a7846f0417. Report an issue: GitHub.