siyuan-note/siyuan · error

a document tree plan must be applied in a single transaction

Error message

a document tree plan must be applied in a single transaction

What it means

AttachTemplateDocTreePlans scans a batch of transactions for one carrying a TemplateDocTreePlanID. A plan is only valid when it is the sole transaction in the batch; if more than one transaction is submitted, or a plan transaction coexists with normal ones, the error is thrown. Document-tree plans must commit atomically so their generated documents can be cleaned up or undone as a unit.

Source

Thrown at kernel/model/template_doc_tree.go:456

		"title":    node.Title,
		"id":       node.ID,
		"parentID": node.ParentID,
		"rootID":   node.RootID,
		"hPath":    node.HPath,
		"name":     "",
		"alias":    "",
	}
}

// AttachTemplateDocTreePlans 将一次性计划转换为内核事务操作,父文档内容与全部子文档共用一条撤销记录。
func AttachTemplateDocTreePlans(transactions []*Transaction) (attached bool, err error) {
	var target *Transaction
	for _, transaction := range transactions {
		if nil == transaction || "" == transaction.TemplateDocTreePlanID {
			continue
		}
		if nil != target || 1 != len(transactions) {
			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")

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Submit the document-tree plan transaction alone in its own /api/transactions request
  2. Split unrelated block edits into a separate, prior or subsequent transaction call
  3. Ensure only one transaction in any single batch carries a TemplateDocTreePlanID
  4. Review retry/queue code so a failed plan transaction is not re-batched with others

Example fix

// before: plan batched with ordinary edits
performTransactions([]*Transaction{planTx, normalTx})
// after: plan committed alone
performTransactions([]*Transaction{planTx})
performTransactions([]*Transaction{normalTx})
Defensive patterns

Strategy: validation

Validate before calling

// Go: ensure a plan transaction is submitted alone
single := len(txs) == 1 && txs[0].TemplateDocTreePlanID != ""
if !single {
    return errors.New("submit the document-tree plan transaction by itself")
}

Try / catch

ok, err := AttachTemplateDocTreePlans(transactions)
if err != nil && strings.Contains(err.Error(), "single transaction") {
    return splitAndRetryIndividually(transactions)
}

Prevention

When it happens

Trigger: Calling the transactions API with an array containing a transaction whose TemplateDocTreePlanID is set alongside another transaction; or including two plan-bearing transactions in one request; performTransactions invokes this during commit.

Common situations: Client-side code batching a template-doc-tree apply with unrelated edits in one request; retry logic re-sending a plan transaction after a partial failure, duplicating it in the queue; plugin code constructing multiple transactions but submitting them together.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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