siyuan-note/siyuan · error

template document tree transaction contains a reserved opera

Error message

template document tree transaction contains a reserved operation

What it means

After structural validation, AttachTemplateDocTreePlans scans every do and undo operation for the reserved actions 'restoreCreatedDoc' and 'removeCreatedDoc'. These actions are owned exclusively by the document-tree plan machinery (used for compensation/cleanup of created documents); a user-supplied transaction containing them is rejected to prevent conflicts with the plan's own generated operations.

Source

Thrown at kernel/model/template_doc_tree.go:474

			return false, errors.New("a document tree plan must be applied in a single transaction")
		}
		target = transaction
	}
	if nil == target {
		return false, nil
	}
	if target.isReplay {
		return false, errors.New("template document tree plans cannot be attached to replay transactions")
	}
	if 0 == len(target.DoOperations) || 0 == len(target.UndoOperations) {
		return false, errors.New("template document tree plan requires reversible parent operations")
	}
	if err = validateTemplateDocTreeParentOperations(target); nil != err {
		return false, err
	}
	for _, operation := range append(append([]*Operation{}, target.DoOperations...), target.UndoOperations...) {
		if nil != operation && ("restoreCreatedDoc" == operation.Action || "removeCreatedDoc" == operation.Action) {
			return false, errors.New("template document tree transaction contains a reserved operation")
		}
	}

	planID := target.TemplateDocTreePlanID
	target.TemplateDocTreePlanID = ""
	value, loaded := templateDocTreePlans.LoadAndDelete(planID)
	if !loaded {
		return false, errors.New("template document tree plan is missing or has expired")
	}
	plan, ok := value.(*templateDocTreePlan)
	if !ok || plan.id != planID || time.Now().After(plan.expiresAt) {
		return false, errors.New("template document tree plan is invalid or has expired")
	}
	if !transactionTargetsTemplateRoot(target, plan.rootID, plan.boxID) {
		return false, errors.New("template document tree plan does not match the edited document")
	}
	rootTree, loadErr := LoadTreeByBlockID(plan.rootID)
	if nil != loadErr || nil == rootTree || rootTree.Box != plan.boxID || rootTree.Path != plan.rootPath ||

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Remove operations with the reserved actions 'restoreCreatedDoc'/'removeCreatedDoc' from the manually built transaction
  2. Let the plan engine generate its own compensation operations — do not pre-seed them
  3. If you need cleanup semantics, rely on undo/redo of the plan transaction rather than emitting the reserved actions yourself
  4. Use distinct custom action names for your own operation types

Example fix

// before: reusing a reserved action
ops = append(ops, &Operation{Action: "removeCreatedDoc", ID: docID})
// after: use a non-reserved action
tx := &Transaction{TemplateDocTreePlanID: planID, DoOperations: doOps, UndoOperations: undoOps}
planEngine.Apply(tx) // engine emits its own compensation ops
Defensive patterns

Strategy: validation

Validate before calling

// Go: reject reserved actions before submission
reserved := map[string]bool{"restoreCreatedDoc": true, "removeCreatedDoc": true}
for _, op := range append(tx.DoOperations, tx.UndoOperations...) {
    if op != nil && reserved[op.Action] {
        return fmt.Errorf("action %q is reserved for the plan engine", op.Action)
    }
}

Try / catch

if _, err := AttachTemplateDocTreePlans([]*Transaction{tx}); err != nil && strings.Contains(err.Error(), "reserved operation") {
    return sanitizeReservedActions(tx)
}

Prevention

When it happens

Trigger: Crafting a transaction whose TemplateDocTreePlanID is set and whose DoOperations/UndoOperations include an operation with Action 'restoreCreatedDoc' or 'removeCreatedDoc'; performTransactions routes it through AttachTemplateDocTreePlans and rejects it.

Common situations: Scripts or tests reusing internal compensation actions in custom transactions; copying operation objects from a previously executed plan transaction into a new one; attempting to manually trigger the cleanup actions that the plan system generates internally.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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