Templater and tp.file.include issues with information shared between templates

Hi. I'd like to share something that I've found out after “investing” a few hours on this task. I am working on modularizing my templates since some time ago (this modularization of mine goes along my older post for template evolution: https://www.reddit.com/r/ObsidianMD/s/C45nNSFC9i / Vault evolution and changes) and during the latest session, I've got stuck with a behavior I couldn't find on Templater docs (at least, not clearly stated, as it was more or less what was present on the examples there).

What I've found is that Templater compiles all tags from all nested includes before executing any script. When a template uses <% myVar %>, Templater tries to resolve it during the parsing phase, i.e., when the file is created and the template is added to it. This occurs before any <%* %> blocks within the parent template are processed in a second step.

During my debugging session, I've tried:

  1. Writing to tData inside an include that calls await tp.file.rename() but I've found that the rename resets the context, so values written inside that include are gone and can't be used by “upper level” templates
  2. Using app._tData to persist this data technically works, but unfortunately the information persists between template executions, so if you create two notes back to back the second one picks up stale data from the first, generating incorrect output on that second note (it is like the template is always one execution behind for the values it will use)
  3. Using <%* tR += ... %> inside includes to protect the output still hits the same issue when the variable doesn't exist yet, so you just add extra markup for no change at all in the final output
sequenceDiagram
    participant User
    participant Templater
    participant Parent as Parent Template
    participant Include as Included Template
    participant File as File

    User->>Templater: Create new file
    Templater->>Parent: Load parent template

    Note over Templater: Phase 1: Parsing
Resolve all <% %> before running scripts Templater->>Include: Load includes
(also parsed first) Include-->>Templater: Fails if <% var %> depends on parent Note over Templater: Phase 2: Execute <%* %> blocks Templater->>Parent: Run initial <%* %> block
Set tData Parent-->>Templater: tData populated Parent->>Templater: Call tp.file.include() Templater->>Include: Execute include
May read tData Include->>Templater: Execute await tp.file.rename() Templater->>File: Rename file Note over Templater: Context reset
tData lost Parent-->>Templater: Continue execution
(but without data written inside include)

The pattern that finally worked for me is what Templater encourages (implicitly) in its own examples:

  1. Do all your prompts and calculations in the initial <%* ... -%> block of the parent template
  2. Store anything you need to share with includes in tData, which must be set before any tp.file.include call
  3. Use <% myVar %> output tags only in the parent body, not inside includes
  4. Keep included templates self-contained, they can read from tData if needed, but never depend on local variables from the parent
%%{init: {'theme': 'base', 'themeVariables': { 'nodeSpacing': 30, 'rankSpacing': 30 }}}%%
graph LR

    subgraph Parent[Parent Template]
        P1[Initial <%* %> block]
        P2[Set tData]
        P3[Body with <% var %>]
    end

    subgraph Includes[Included Templates]
        I1[Read tData]
        I2[Do not use parent's <% var %>]
        I3[May call rename
context lost] end subgraph DataBus[tData] T1[Values computed in parent] end P1 --> P2 P2 --> DataBus DataBus --> I1 Parent --> Includes Includes --> Parent

For example, you can use something like this parent template:

<%*
const dateInput = await tp.system.prompt("Date (YYYY-MM-DD)", moment().format("YYYY-MM-DD"));
const baseDate = moment(dateInput, "YYYY-MM-DD");
const day = baseDate.format("YYYY-MM-DD");
tData = { baseDate: baseDate };
-%>
---
<% tp.file.include("[[Your Frontmatter Template]]") %>
---
# Event on <% day %>

and this included “Your Frontmatter Template.md”:

<%*
let tt = (typeof tData !== 'undefined' && tData.baseDate) ? tData.baseDate : moment();
-%>
date: "<% tt.format('YYYY-MM-DD') %>"
weekyear: "<% tt.format('gggg-[W]ww') %>"

In the end tData will work as a “data bus” between templates as long as you set it in the parent template before any include. The moment you try to write to it inside an include that does await tp.file.rename(), you lose the values. And any <% %> output tag inside an include that references a variable from the parent will fail at parse time, before anything runs.

%%{init: {'theme': 'base', 'themeVariables': { 'nodeSpacing': 30, 'rankSpacing': 30 }}}%%
flowchart TD

    A[Start of Parent Template] --> B[Execute initial <%* %> block]
    B --> C[Collect inputs and compute values]
    C --> D[Populate tData]
    D --> E{Is tData defined before includes?}

    E -->|No| F[Error: includes cannot access data]
    E -->|Yes| G[Call tp.file.include]

    G --> H[Include executes
May read tData] H --> I{Does include call tp.file.rename?} I -->|Yes| J[Context reset
tData lost] I -->|No| K[Normal execution] J --> L[Data sharing fails] K --> M[Return to parent template] M --> N[Use <% var %> only in parent] N --> O[End]

Hope this saves someone a few hours of testing and debugging.

Published, without the diagrams, at: Reddit