siyuan-note/siyuan · error

template document tree plan is missing or has expired

Error message

template document tree plan is missing or has expired

What it means

AttachTemplateDocTreePlans looks up the pending template document tree plan by the ID stored on the transaction (TemplateDocTreePlanID) using sync.Map.LoadAndDelete. This error means no plan is registered under that ID: the plan was never created, was already consumed by a previous attach, or the TTL timer (time.AfterFunc) evicted it. Plans are one-shot and in-memory only, so any replay or retry of the same transaction with a stale plan ID fails here.

Source

Thrown at kernel/model/template_doc_tree.go:482

		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 ||
		rootTree.HPath != plan.rootHPath {
		return false, errors.New("the document used to render the template has changed")
	}
	target.templateDocTreeRootSnapshot = rootTree
	box := Conf.Box(plan.boxID)
	if nil == box {
		return false, ErrBoxNotFound
	}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Re-render the template to obtain a fresh plan ID and send a new transaction instead of retrying with the old TemplateDocTreePlanID
  2. Reduce the delay between plan creation and transaction submission so it stays within templateDocTreePlanTTL
  3. Ensure each plan ID is used exactly once; do not reuse it for retries or duplicates
  4. Verify the client talks to the same running kernel instance that created the plan (no restart or multi-process split)

Example fix

// before
transaction.TemplateDocTreePlanID = stalePlanID // reused after a failed attempt
performTransactions([]*Transaction{transaction})
// after
planID, summary := renderTemplateDocTree(...) // re-render to get a fresh one-shot plan
transaction.TemplateDocTreePlanID = planID
performTransactions([]*Transaction{transaction})
Defensive patterns

Strategy: retry

Validate before calling

if planID == "" || time.Since(planCreatedAt) > planTTL || planConsumed[planID] { re-render plan before submitting }

Try / catch

err := attachAndPerform(tx); if err != nil && strings.Contains(err.Error(), "plan is missing or has expired") { planID = reRenderPlan(); tx.TemplateDocTreePlanID = planID; retry once }

Prevention

When it happens

Trigger: Calling performTransactions with a transaction whose TemplateDocTreePlanID references (a) an ID never registered via the plan collector, (b) a plan already consumed by an earlier AttachTemplateDocTreePlans call (LoadAndDelete removes it), (c) a plan older than templateDocTreePlanTTL, or (d) a kernel restart which wiped the in-memory map.

Common situations: Retrying a failed HTTP transaction request with the same plan ID; a slow client that waits longer than the plan TTL before committing; undo/redo replay reusing a recorded plan ID; multiple kernel instances behind a proxy where the plan was created in another process.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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