## Overview
Templater is a template plugin for Obsidian.md that defines a templating language for inserting variables and function results into notes. It executes JavaScript code to manipulate variables and automate tasks.
**Engine**: rusty_engine (custom templating engine)
**License**: GNU AGPLv3
## Core Syntax
### Command Structure
All commands use opening `<%` and closing `%>` tags.
**Basic Interpolation**: `<% expression %>`
```markdown
<% tp.date.now() %>
```
**JavaScript Execution**: `<%* code %>`
```javascript
<%*
let value = await tp.system.prompt("Enter value");
tR += value; // Output to template result
%>
```
**Dynamic Commands** (preview mode only): `<%+ expression %>`
```markdown
Last modified: <%+ tp.file.last_modified_date() %>
```
_Note: Dynamic commands are deprecated; use Dataview plugin instead._
### Whitespace Control
- `<%_` - Trim all whitespace before command
- `_%>` - Trim all whitespace after command
- `<%-` - Trim one newline before command
- `-%>` - Trim one newline after command
```markdown
<%_ if (tp.file.title == "MyFile") { -%>
Content here
<%_ } -%>
```
### Function Invocation
Functions use dot notation under the `tp` object:
```javascript
tp.<module>.<function>(arg1, arg2, ...)
```
**Argument Types**:
- `string`: Quoted values (`"text"` or `'text'`)
- `number`: Integers (`15`, `-5`)
- `boolean`: `true` or `false` (lowercase)
**Documentation Notation** (for reference only):
```typescript
tp.function(arg?: type = default, arg2: type1|type2)
```
- `?` = optional argument
- `= value` = default value
- `|` = multiple type options
## Internal Modules
### tp.date
Date manipulation functions using moment.js format strings.
**tp.date.now(format?, offset?, reference?, reference_format?)**
```javascript
<% tp.date.now() %> // 2024-01-15
<% tp.date.now("Do MMMM YYYY") %> // 15th January 2024
<% tp.date.now("YYYY-MM-DD", -7) %> // Last week
<% tp.date.now("YYYY-MM-DD", "P-1M") %> // Last month (ISO 8601)
<% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %> // Tomorrow from file title date
```
**tp.date.tomorrow(format?)**
```javascript
<% tp.date.tomorrow("YYYY-MM-DD") %>
```
**tp.date.yesterday(format?)**
```javascript
<% tp.date.yesterday("YYYY-MM-DD") %>
```
**tp.date.weekday(format?, weekday, reference?, reference_format?)**
```javascript
<% tp.date.weekday("YYYY-MM-DD", 0) %> // This week's Monday
<% tp.date.weekday("YYYY-MM-DD", 7) %> // Next Monday
<% tp.date.weekday("YYYY-MM-DD", -7, tp.file.title, "YYYY-MM-DD") %> // Previous Monday from reference
```
**Direct moment.js access**:
```javascript
<% moment(tp.file.title, "YYYY-MM-DD").endOf("month").format("YYYY-MM-DD") %>
```
### tp.file
File operations and metadata.
**Properties**:
- `tp.file.content` - File contents (read-only string)
- `tp.file.title` - File basename without extension
- `tp.file.path(relative?)` - File path (absolute if `relative=false`)
- `tp.file.tags` - Array of file tags
**tp.file.creation_date(format?)**
```javascript
<% tp.file.creation_date() %> // 2024-01-15 14:30
<% tp.file.creation_date("dddd Do MMMM YYYY HH:mm") %>
```
**tp.file.last_modified_date(format?)**
```javascript
<% tp.file.last_modified_date() %>
<% tp.file.last_modified_date("YYYY-MM-DD HH:mm:ss") %>
```
**tp.file.cursor(order?)**
Set cursor position after template insertion. Same order = multi-cursor.
```javascript
<% tp.file.cursor() %>
<% tp.file.cursor(1) %>text<% tp.file.cursor(1) %> // Multi-cursor
```
**tp.file.cursor_append(content)**
Append content after active cursor.
```javascript
<% tp.file.cursor_append("Some text") %>
```
**tp.file.exists(filepath)** (async)
```javascript
<%* if (await tp.file.exists("path/to/file.md")) { %> File exists <%* } %>
```
**tp.file.find_tfile(filename)**
Returns TFile object for given filename.
```javascript
<%* let tfile = tp.file.find_tfile("MyNote") %>
<%* const currentFile = tp.file.find_tfile(tp.file.path(true)) %> // Get current file
```
**tp.file.folder(relative?)**
```javascript
<% tp.file.folder() %> // Folder name
<% tp.file.folder(true) %> // Path/To/Folder
```
**tp.file.include(include_link)** (async)
Include and resolve another file's content.
```javascript
<% await tp.file.include("[[Template1]]") %>
<% await tp.file.include(tp.file.find_tfile("MyFile")) %>
<% await tp.file.include("[[MyFile#Section1]]") %> // Section
<% await tp.file.include("[[MyFile#^block1]]") %> // Block
```
**tp.file.create_new(template, filename?, open_new?, folder?)** (async)
```javascript
<%* await tp.file.create_new("Content", "MyFile") %>
<%* await tp.file.create_new(tp.file.find_tfile("Template"), "MyFile") %>
<%* await tp.file.create_new("Content", "MyFile", true) %> // Open
<%* await tp.file.create_new("Content", "MyFile", false, tp.file.folder(true)) %>
<%* await tp.file.create_new("Content", "MyFile", false, "Path/To/Folder") %>
```
**tp.file.move(new_path, file_to_move?)**
```javascript
<% await tp.file.move("/Notes/MyNote") %> // Move current file
```
**tp.file.rename(new_title)**
```javascript
<% await tp.file.rename("New Title") %>
```
**tp.file.selection()**
Returns selected text in editor.
```javascript
<% tp.file.selection() %>
```
### tp.frontmatter
Access frontmatter variables.
```yaml
---
alias: myfile
note type: seedling
tags: [tag1, tag2]
---
```
```javascript
<% tp.frontmatter.alias %> // myfile
<% tp.frontmatter["note type"] %> // For keys with spaces
<% tp.frontmatter.tags.join(", ") %> // Array methods work
```
**Note**: `tp.frontmatter` may return empty/undefined during file creation. Use `tp.hooks.on_all_templates_executed()` to modify frontmatter after template execution.
### tp.system
User interaction functions.
**tp.system.clipboard()**
```javascript
<% tp.system.clipboard() %>
```
**tp.system.prompt(prompt_text?, default_value?, throw_on_cancel?, multiline?)** (async)
```javascript
<% await tp.system.prompt("Enter value") %>
<% await tp.system.prompt("Mood?", "happy") %>
<% await tp.system.prompt("Notes", null, false, true) %> // Multiline
<%* let value = await tp.system.prompt("Enter value"); %>
Value: <% value %>
```
**tp.system.suggester(text_items, items, throw_on_cancel?, placeholder?, limit?)** (async)
```javascript
<% await tp.system.suggester(["Happy", "Sad"], ["😊", "😢"]) %>
<% await tp.system.suggester(item => item, ["Option1", "Option2"]) %>
// Files suggester
[[<% (await tp.system.suggester(item => item.basename, tp.app.vault.getMarkdownFiles())).basename %>]]
// Tags suggester
<% await tp.system.suggester(item => item, Object.keys(tp.app.metadataCache.getTags()).map(x => x.replace("#", ""))) %>
<%* let selected = await tp.system.suggester(["A", "B"], [1, 2]); %>
Selected: <% selected %>
```
**tp.system.multi_suggester(text_items, items, throw_on_cancel?, title?, limit?)** (async)
Multiple selection variant.
```javascript
<% await tp.system.multi_suggester(["A", "B", "C"], ["A", "B", "C"]) %>
<% (await tp.system.multi_suggester(item => item.basename, tp.app.vault.getMarkdownFiles())).map(f => `[[${f.basename}]]`) %>
```
### tp.web
Web request functions (all async).
**tp.web.daily_quote()**
```javascript
<% await tp.web.daily_quote() %>
```
**tp.web.random_picture(size?, query?, include_size?)**
```javascript
<% await tp.web.random_picture() %>
<% await tp.web.random_picture("200x200") %>
<% await tp.web.random_picture("200x200", "landscape,water") %>
```
**tp.web.request(url, path?)**
```javascript
<% await tp.web.request("https://api.example.com/data") %>
<% await tp.web.request("https://api.example.com/todos", "0.title") %> // Extract path
```
### tp.config
Templater running configuration (for scripts).
**Properties**:
- `tp.config.active_file` - Active file when Templater launched (if exists)
- `tp.config.run_mode` - How Templater was launched (RunMode enum)
- `tp.config.target_file` - TFile where template will be inserted
- `tp.config.template_file` - TFile of the template
### tp.hooks
Execute code on Templater events.
**tp.hooks.on_all_templates_executed(callback_function)**
Runs after all templates finish executing. Multiple calls run in parallel.
```javascript
<%*
tp.hooks.on_all_templates_executed(async () => {
const file = tp.file.find_tfile(tp.file.path(true));
await tp.app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter["key"] = "value";
});
});
%>
<%*
tp.hooks.on_all_templates_executed(() => {
tp.app.commands.executeCommandById("obsidian-linter:lint-file");
});
%>
```
### tp.app
Exposes Obsidian app instance. Use instead of global `app`.
```javascript
// Get all folders
<% tp.app.vault.getAllLoadedFiles()
.filter(x => x instanceof tp.obsidian.TFolder)
.map(x => x.name) %>
// Update frontmatter
<%*
const file = tp.file.find_tfile("path/to/file");
await tp.app.fileManager.processFrontMatter(file, (fm) => {
fm["key"] = "value";
});
%>
// Read file content
<%*
const file = tp.file.find_tfile(tp.file.path(true));
const content = await app.vault.read(file);
%>
```
### tp.obsidian
Exposes Obsidian API classes and functions.
```javascript
// Get all folders using TFolder class
<% tp.app.vault.getAllLoadedFiles()
.filter(x => x instanceof tp.obsidian.TFolder)
.map(x => x.name) %>
// Normalize path
<% tp.obsidian.normalizePath("Path/to/file.md") %>
// HTML to markdown
<% tp.obsidian.htmlToMarkdown("<h1>Title</h1><p>Text</p>") %>
// HTTP request
<%*
const response = await tp.obsidian.requestUrl("https://api.example.com/data");
tR += response.json.title;
%>
```
## User Functions
### Script User Functions
JavaScript files in configured script folder become functions under `tp.user`.
**Setup**: Settings → Script folder location
**Script file** (`Scripts/my_script.js`):
```javascript
function myFunction(msg) {
return `Message: ${msg}`;
}
module.exports = myFunction;
// Or export object with multiple functions
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
```
**Usage**:
```javascript
<% tp.user.my_script("Hello") %>
<% tp.user.my_script.add(5, 3) %> // If exporting object
// Pass tp object to access Templater API
<% tp.user.my_script(tp) %>
```
**Script with tp access**:
```javascript
function myFunction(tp, arg) {
return `File: ${tp.file.title}, Arg: ${arg}`;
}
module.exports = myFunction;
```
**TSDoc support** for intellisense:
```javascript
/**
* Calculates sum of two numbers
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} Sum of a and b
*/
function add(a, b) {
return a + b;
}
module.exports = add;
```
**Type Definitions for Enhanced IntelliSense**
For full TypeScript-like functionality and autocomplete in your JavaScript user scripts, use the `templater-scripts-types` package:
**Setup**:
1. Clone the types repository: `gh repo clone TheRealWolfick/templater-scripts-types`
2. Copy `.dev` folder and `tsconfig.json` to your Templater scripts folder
3. (Optional) Add `**/.dev/**` and `**/tsconfig.json` to `.gitignore` if using Obsidian Git
**Script template with types**:
```javascript
//import moment from '.dev/types/moment';
/**
* @param {import('templater-obsidian').TemplaterApi} tp
*/
function userFunction(tp) {
// Full autocomplete and type checking available
const title = tp.file.title;
const date = tp.date.now("YYYY-MM-DD");
return `${title} - ${date}`;
}
module.exports = userFunction;
```
**With Dataview integration**:
```javascript
//import moment from '.dev/types/moment';
/**
* @param {import('templater-obsidian').TemplaterApi} tp
* @param {import('api/inline-api').DataviewInlineApi} dv
*/
function userFunction(tp, dv) {
const pages = dv.pages("#project");
return pages.length;
}
module.exports = userFunction;
```
**Usage in template**:
```javascript
<% tp.user.userFunction(tp) %>
<% tp.user.userFunction(tp, this.app.plugins.plugins["dataview"].api) %>
```
**Note on moment.js**: Uncomment the `import moment` line for types/autocomplete during development, then re-comment before execution (Templater will throw an error if the import is active).
**Benefits**:
- Full IntelliSense for all Templater API functions
- JSDoc documentation on hover
- Type checking for function parameters
- Autocomplete for Obsidian API methods
- Reduced errors through compile-time validation
**Note**: Scripts can't access `tR` directly from their scope. To use `tR` in user scripts, pass it as a parameter:
```javascript
/**
* @param {import('templater-obsidian').TemplaterApi} tp
* @param {string} tR - Template result variable for output control
*/
function userFunction(tp, tR) {
// Now tR can be used within the function
return "
---\ntype: note\n
---\n";
}
module.exports = userFunction;
```
**Usage in template**:
```javascript
<%* tR = ""; tR += await tp.user.userFunction(tp, tR) %>
```
This pattern is essential for scripts that need to:
- Prepend content (like frontmatter) to the document
- Clear existing output with `tR = ""`
- Build complex output programmatically
### System Command User Functions
Execute system commands as user functions.
**Setup**: Settings → User System Command Functions
Configure command name and shell command. Internal functions resolved before execution.
```javascript
// If command "echo" is configured as: echo "{{VALUE}}"
<% tp.user.echo({VALUE: "hello"}) %>
// Command with internal function: cat <% tp.file.path() %>
<% tp.user.cat_file() %> // Executes: cat /path/to/file.md
```
Arguments passed as environment variables to the system command.
## JavaScript Execution Commands
### Output Control
Use `tR` variable to control output:
```javascript
<%* tR += "appended text" %> // Append to output
<%* tR = "" %> // Clear all previous output
```
**Example - Conditional frontmatter**:
```markdown
---
type: template
---
Template content here
<%\* tR = "" -%>
---
## type: person
# <% tp.file.cursor() %>
```
Output becomes:
```markdown
---
type: person
---
#
```
### Async Functions
Many functions are async - use `await`:
```javascript
<%*
const value = await tp.system.prompt("Enter value");
await tp.file.create_new("Content", "Filename");
%>
```
### Control Flow Examples
**Conditional**:
```javascript
<%* if (tp.file.title.startsWith("Daily")) { -%>
This is a daily note!
<%* } else { -%>
Regular note
<%* } -%>
<%* if (tp.frontmatter.type === "seedling") { -%>
🌱 Seedling content
<%* } -%>
<%* if (tp.file.tags.contains("#todo")) { -%>
This is a todo!
<%* } -%>
```
**Loops**:
```javascript
<%*
const files = tp.app.vault.getMarkdownFiles();
for (let file of files) {
tR += `- [[${file.basename}]]\n`;
}
%>
```
**Functions**:
```javascript
<%*
function formatDate(date) {
return moment(date).format("YYYY-MM-DD");
}
let today = formatDate(new Date());
%>
Date: <% today %>
```
**String manipulation**:
```javascript
<%* tR += tp.file.content.replace(/old/g, "new") %>
```
## Settings
### General Settings
- **Template folder location**: Files in folder available as templates
- **Syntax Highlighting**: Desktop/mobile edit mode highlighting
- **Automatic jump to cursor**: Auto-trigger `tp.file.cursor` after insert
- **Trigger on new file creation**: Auto-apply templates on file creation
### Template Hotkeys
Bind templates to keyboard shortcuts.
### Folder Templates
Auto-apply templates to folders (requires "Trigger on new file creation").
Deepest match used. Add `/` rule for catch-all.
### File Regex Templates
Apply templates based on regex path matching (requires "Trigger on new file creation").
First match used. End with `.*` for catch-all.
### Startup Templates
Templates executed once on Templater startup. No output. Useful for registering hooks.
### User Script Functions
Folder for JavaScript files (CommonJS modules) as user functions.
### User System Command Functions
Configure system commands as user functions.
⚠️ **Security Warning**: Only execute trusted code/commands from known sources.
## Advanced Patterns
### Modify Frontmatter After Execution
```javascript
<%*
tp.hooks.on_all_templates_executed(async () => {
const file = tp.file.find_tfile(tp.file.path(true));
await tp.app.fileManager.processFrontMatter(file, (fm) => {
fm.created = tp.date.now();
fm.modified = tp.date.now();
});
});
%>
```
### Template File Creation with Link
```javascript
[[<%*
const newFile = await tp.file.create_new("Content", "NewNote");
tR += newFile.basename;
%>]]
```
### Nested User Functions (Mobile Workaround)
Pass arguments via `tp` properties with `tp.file.include()`:
**Caller**:
```javascript
<%*
tp.name = "Ryan";
await tp.file.include('[[SayHello]]');
%>
```
**SayHello template**:
```javascript
Hello <% tp.name %>!
```
### File Exists Check
```javascript
<%*
const path = "path/to/file.md";
if (await tp.file.exists(path)) {
tR += "File exists";
} else {
tR += "File not found";
}
%>
```
### Dynamic File List
```javascript
<%*
const files = tp.app.vault.getMarkdownFiles()
.filter(f => f.path.startsWith("Notes/"))
.sort((a, b) => a.basename.localeCompare(b.basename));
for (let file of files) {
tR += `- [[${file.basename}]]\n`;
}
%>
```
### Complex Suggester with Files
```javascript
<%*
const files = tp.app.vault.getMarkdownFiles()
.filter(f => !f.path.includes("Archive"));
const selected = await tp.system.suggester(
f => `${f.basename} (${f.parent.name})`,
files,
false,
"Select a file"
);
if (selected) {
tR += `[[${selected.basename}]]`;
}
%>
```
## Common Patterns
### Daily Note Template
```markdown
---
date: <% tp.date.now("YYYY-MM-DD") %>
weekday: <% tp.date.now("dddd") %>
created: <% tp.file.creation_date() %>
---
# <% tp.date.now("dddd, MMMM DD, YYYY") %>
<< [[<% tp.date.now("YYYY-MM-DD", -1) %>]] | [[<% tp.date.now("YYYY-MM-DD", 1) %>]] >>
## Notes
<% tp.file.cursor() %>
## Links
```
### Meeting Note Template
```markdown
---
date: <% tp.date.now() %>
participants: <% await tp.system.prompt("Participants") %>
tags: [meeting]
---
# Meeting: <% await tp.system.prompt("Meeting Title") %>
**Date**: <% tp.date.now("YYYY-MM-DD HH:mm") %>
**Participants**: <% await tp.system.prompt("Participants") %>
## Agenda
<% tp.file.cursor(1) %>
## Notes
<% tp.file.cursor(2) %>
## Action Items
<% tp.file.cursor(3) %>
```
### Person Template
```markdown
---
type: person
name: <% tp.file.title %>
created: <% tp.file.creation_date() %>
---
# <% tp.file.title %>
## Contact Info
<% tp.file.cursor(1) %>
## Notes
<% tp.file.cursor(2) %>
## Related
```
## Troubleshooting
### Frontmatter Returns Undefined
- `tp.frontmatter` may be empty on file creation
- Use `tp.hooks.on_all_templates_executed()` to access frontmatter after template execution
- Ensure frontmatter format is correct (YAML between `
---` markers)
### Dynamic Commands Not Working
- Dynamic commands deprecated, use Dataview plugin
- Only execute in preview mode, not live preview
- Cached in preview, won't refresh automatically
### Template Not Executing
- Check "Trigger on new file creation" is enabled
- Verify Folder Templates or File Regex rules are configured
- Check template folder location in settings
- Verify file doesn't have syntax errors
### Async Function Issues
- Always use `await` with async functions
- Use `<%*` execution commands for async operations
- Prompts/suggesters require `await` to capture values
### Performance with Large Lists
- Use `limit` parameter in suggester functions
- Filter data before passing to suggester
- Consider pagination for very large datasets
## Best Practices
1. **Use execution commands** (`<%*`) for complex logic
2. **Always await** async functions (prompts, file operations, web requests)
3. **Use whitespace control** (`-%>`) to clean up output
4. **Store reusable code** in user script functions
5. **Use `tp.hooks`** for post-template operations (frontmatter updates)
6. **Prefer `tp.app`** over global `app` for consistency
7. **Test templates** on sample files before production use
8. **Use `tR` carefully** - understand when to append vs. overwrite
9. **Leverage `tp.file.include()`** for template composition
10. **Document custom scripts** with TSDoc for intellisense
## Security Considerations
- ⚠️ Can execute arbitrary JavaScript code
- ⚠️ Can run system commands
- ⚠️ Only use trusted templates and scripts
- ⚠️ Review user functions before enabling
- ⚠️ Be cautious with "Trigger on new file creation"
- ⚠️ System commands expose environment variables
## Resources
- **Documentation**: https://silentvoid13.github.io/Templater/
- **GitHub**: https://github.com/SilentVoid13/Templater
- **Obsidian API**: https://docs.obsidian.md/
- **Moment.js Format**: https://momentjs.com/docs/#/displaying/format/
- **Template Showcases**: GitHub Discussions / Community forums