Something interesting and powerful to share when you want to add one or multiple custom properties to the current note.
You define the property, or properties, inside a table in a “property note”. I chose Extras/properties to add as many notes as I need.
The properties note will look like this:
or
or
This is how you call the templater note:
<%* await tp.user["add-properties-from-template"](tp) %>
This is the templater script saved under your templater scripts folder:
// File: add-properties-from-template.js
// Templater script to add properties from property definition notes
// Place this in your Templater scripts folder
//
// Oct 18: Modified to prevent ovewriting an existing property values
module.exports = async (tp) => {
const app = tp.app;
const propertiesFolder = "Extras/properties";
try {
// Get all files in the properties folder
const folder = app.vault.getAbstractFileByPath(propertiesFolder);
if (!folder) {
new Notice(`Properties folder not found: ${propertiesFolder}`);
return "";
}
// Get all markdown files in the folder
const propertyFiles = app.vault.getMarkdownFiles()
.filter(file => file.path.startsWith(propertiesFolder + "/"));
if (propertyFiles.length === 0) {
new Notice("No property definition notes found in " + propertiesFolder);
return "";
}
// Create suggester options
const fileNames = propertyFiles.map(f => f.basename);
// Show suggester to user
const selectedFileName = await tp.system.suggester(fileNames, fileNames, false, "Select a property template:");
if (!selectedFileName) {
return ""; // User cancelled
}
// Find the selected file
const selectedFile = propertyFiles.find(f => f.basename === selectedFileName);
const content = await app.vault.read(selectedFile);
// Parse the table
const parsedProperties = parsePropertiesTable(content);
if (!parsedProperties) {
new Notice("⚠️ No valid property table found in the selected note");
await createExampleNote(app);
return "";
}
if (parsedProperties.length === 0) {
new Notice("⚠️ Table format is incorrect. Check the example note.");
await createExampleNote(app);
return "";
}
// Get current file and its properties
const currentFile = app.workspace.getActiveFile();
// Store properties in outer scope so the callback can access them
const propertiesToAdd = parsedProperties;
await app.fileManager.processFrontMatter(currentFile, (frontmatter) => {
let addedCount = 0;
let skippedCount = 0;
propertiesToAdd.forEach(prop => {
if (!(prop.name in frontmatter)) {
// Property doesn't exist, add it
frontmatter[prop.name] = prop.value;
addedCount++;
} else {
// Property exists, skip it
skippedCount++;
}
});
// Build notification message
const messages = [];
if (addedCount > 0) messages.push(`${addedCount} added`);
if (skippedCount > 0) messages.push(`${skippedCount} skipped (already exist)`);
if (messages.length > 0) {
new Notice(`✓ Properties updated: ${messages.join(', ')}`);
} else {
new Notice("No changes made");
}
});
} catch (error) {
console.error("Error adding properties:", error);
new Notice("Error: " + error.message);
}
return "";
// Helper functions defined inside the main function
function parsePropertiesTable(content) {
// Find markdown table in content
const lines = content.split('\n');
let inTable = false;
let headerFound = false;
const properties = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Check if this is a table row
if (line.startsWith('|') && line.endsWith('|')) {
const cells = line.split('|')
.map(cell => cell.trim())
.filter(cell => cell.length > 0);
// Check for header row
if (!headerFound && cells.length === 2) {
if (cells[0].toLowerCase() === 'name' && cells[1].toLowerCase() === 'value') {
headerFound = true;
inTable = true;
continue;
}
}
// Skip separator row
if (inTable && line.includes('---')) {
continue;
}
// Parse data rows
if (inTable && headerFound && cells.length === 2) {
const name = cells[0];
let value = cells[1];
// Parse value - try to detect arrays
value = parseValue(value);
properties.push({ name, value });
}
} else if (inTable) {
// End of table
break;
}
}
return headerFound ? properties : null;
}
function parseValue(valueStr) {
valueStr = valueStr.trim();
// Handle empty values - return empty string instead of skipping
if (valueStr === '' || valueStr === '""' || valueStr === "''") {
return '';
}
// Check if it looks like an array (contains commas outside of links)
// Simple heuristic: if there are commas, try to parse as array
if (valueStr.includes(',')) {
// Split by comma, but be careful with [[links]]
const parts = [];
let current = '';
let inLink = false;
for (let i = 0; i < valueStr.length; i++) {
const char = valueStr[i];
const nextChar = valueStr[i + 1];
if (char === '[' && nextChar === '[') {
inLink = true;
current += char;
} else if (char === ']' && nextChar === ']') {
inLink = false;
current += char;
} else if (char === ',' && !inLink) {
parts.push(current.trim());
current = '';
} else {
current += char;
}
}
if (current.trim()) {
parts.push(current.trim());
}
if (parts.length > 1) {
return parts;
}
}
// Return as-is for single values
return valueStr;
}
async function createExampleNote(app) {
const exampleContent = `# Property Table Example
Properties must be defined in a table with this exact format:
| name | value |
| ---- | ----- |
| collection | [[my-collection]] |
| context | Machine Learning |
| related | [[note1]], [[note2]] |
| status | active |
| priority | 1 |
| description | |
**Important:**
- Table must have exactly two columns: "name" and "value"
- Multiple values can be comma-separated
- Links should use [[wiki-link]] format
- Text, numbers, and links are preserved as-is
- Empty values are allowed - just leave the value cell blank or use ""
**Behavior when property exists:**
- **Existing properties are skipped** - their values won't be changed
- Only new properties are added
`;
const fileName = "Property Table Example.md";
const filePath = `${fileName}`;
try {
// Check if file already exists
const existingFile = app.vault.getAbstractFileByPath(filePath);
if (!existingFile) {
await app.vault.create(filePath, exampleContent);
new Notice(`Created example note: ${fileName}`);
// Open the example note
const newFile = app.vault.getAbstractFileByPath(filePath);
await app.workspace.getLeaf().openFile(newFile);
} else {
new Notice(`Example note already exists: ${fileName}`);
await app.workspace.getLeaf().openFile(existingFile);
}
} catch (error) {
console.error("Error creating example note:", error);
}
}
};
Assign a hotkey to the templater note, something like Alt+P. Then, in the current note, just press the hotkey, and select the property note you want to append to your frontmatter.
This is how the suggester would look after adding property-notes with their respective property:value pairs tables:
Features
- Dynamic definition of properties to append to several notes
- Properties are entered in a table inside a note
- Table with two columns: name and value, just as any dictionary
- Values can be text, numbers, links, lists, etc. Can be empty too
- Several property-notes allowed in a common folder
- Property-notes can be selected from a suggester that is called via a hotkey
- Core script is a templater JavaScript living in the typical templater script folder
- The core templater user script is called from a one-liner in a standard templater note
- Values can be skipped, overwritten, or appended
- Error handling when table format is incorrect, or selected note doesn’t have a table
Enjoy!
EDIT
Oct 18: Modified to prevent ovewriting an existing property values





