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
- Re-render the template to obtain a fresh plan ID and send a new transaction instead of retrying with the old TemplateDocTreePlanID
- Reduce the delay between plan creation and transaction submission so it stays within templateDocTreePlanTTL
- Ensure each plan ID is used exactly once; do not reuse it for retries or duplicates
- 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
- Apply each plan exactly once immediately after rendering
- Never reuse a plan ID across retries; re-render instead
- Keep render-to-apply latency well under the plan TTL
- Assume plans are lost on kernel restart and re-render after any restart
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
- template document tree plan is invalid or has expired
- template document tree plan does not match the edited docume
- the document used to render the template has changed
- attribute view [%s] view [%s] not found
- attribute view [%s] has no available view: %w
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/c5d8e7c6c016b814.
Report an issue: GitHub.