# Script API Cookbook ## Table of Contents 1. [Introduction](#introduction) 2. [Backend Script Recipes](#backend-script-recipes) 3. [Frontend Script Recipes](#frontend-script-recipes) 4. [Common Patterns](#common-patterns) 5. [Note Manipulation](#note-manipulation) 6. [Attribute Operations](#attribute-operations) 7. [Search and Filtering](#search-and-filtering) 8. [Automation Examples](#automation-examples) 9. [Integration with External Services](#integration-with-external-services) 10. [Custom Widgets](#custom-widgets) 11. [Event Handling](#event-handling) 12. [Best Practices](#best-practices) ## Introduction Trilium's Script API provides powerful automation capabilities through JavaScript code that runs either on the backend (Node.js) or frontend (browser). This cookbook contains practical recipes and patterns for common scripting tasks. ### Script Types | Type | Environment | Access | Use Cases | |------|------------|--------|-----------| | **Backend Script** | Node.js | Full database, file system, network | Automation, data processing, integrations | | **Frontend Script** | Browser | UI manipulation, user interaction | Custom widgets, UI enhancements | | **Custom Widget** | Browser | Widget lifecycle, note context | Interactive components, visualizations | ### Basic Script Structure **Backend Script:** ```javascript // Access to api object is automatic const note = await api.getNoteWithLabel('todoList'); const children = await note.getChildNotes(); // Return value becomes script output return { noteTitle: note.title, childCount: children.length }; ``` **Frontend Script:** ```javascript // Access to api object is automatic api.showMessage('Script executed!'); // Manipulate UI const $button = $(' `); // Create overlay const $overlay = $(`
`); // Add to page $('body').append($button, $overlay, $modal); // Handle button click $button.click(() => { $overlay.show(); $modal.show(); $('#quick-note-title').focus(); }); // Handle save $('#quick-note-save').click(async () => { const title = $('#quick-note-title').val() || 'Quick Note'; const content = $('#quick-note-content').val() || ''; const type = $('#quick-note-type').val(); let finalContent = content; // Format based on type if (type === 'task') { finalContent = `

📋 ${title}

`; } else if (type === 'code') { finalContent = `// ${title}\n${content}`; } else { finalContent = `

${title}

${content}

`; } // Get current note or use inbox const currentNote = api.getActiveContextNote(); const parentNoteId = currentNote ? currentNote.noteId : (await api.getDayNote()).noteId; // Create note const { note } = await api.runOnBackend(async (parentId, noteTitle, noteContent, noteType) => { const parent = await api.getNote(parentId); const newNote = await api.createNote(parent, noteTitle, noteContent, noteType === 'code' ? 'code' : 'text'); if (noteType === 'task') { await newNote.setLabel('task'); await newNote.setLabel('created', api.dayjs().format()); } return { note: newNote.getPojo() }; }, [parentNoteId, title, finalContent, type]); api.showMessage(`Note "${title}" created!`); // Clear and close $('#quick-note-title').val(''); $('#quick-note-content').val(''); $overlay.hide(); $modal.hide(); // Navigate to new note await api.activateNewNote(note.noteId); }); // Handle cancel $('#quick-note-cancel, #quick-note-overlay').click(() => { $overlay.hide(); $modal.hide(); }); // Keyboard shortcuts $(document).keydown((e) => { // Ctrl+Shift+N to open quick note if (e.ctrlKey && e.shiftKey && e.key === 'N') { e.preventDefault(); $button.click(); } // Escape to close if (e.key === 'Escape' && $modal.is(':visible')) { $overlay.hide(); $modal.hide(); } }); ``` ### 7. Note Graph Visualizer Create an interactive graph of note relationships: ```javascript // Load D3.js await api.requireLibrary('d3'); // Create container const $container = $(`
`); // Add to current note const $noteDetail = $(`.note-detail-code`); $noteDetail.empty().append($container); // Get note data const graphData = await api.runOnBackend(async () => { const currentNote = api.getActiveContextNote(); const maxDepth = 3; const nodes = []; const links = []; const visited = new Set(); async function traverse(note, depth = 0) { if (!note || depth > maxDepth || visited.has(note.noteId)) { return; } visited.add(note.noteId); nodes.push({ id: note.noteId, title: note.title, type: note.type, depth: depth }); // Get children const children = await note.getChildNotes(); for (const child of children) { links.push({ source: note.noteId, target: child.noteId, type: 'child' }); await traverse(child, depth + 1); } // Get relations const relations = await note.getRelations(); for (const relation of relations) { const targetNote = await relation.getTargetNote(); if (targetNote) { links.push({ source: note.noteId, target: targetNote.noteId, type: 'relation', name: relation.name }); if (!visited.has(targetNote.noteId)) { nodes.push({ id: targetNote.noteId, title: targetNote.title, type: targetNote.type, depth: depth + 1 }); visited.add(targetNote.noteId); } } } } await traverse(currentNote); return { nodes, links }; }); // Create D3 visualization const width = $container.width(); const height = $container.height(); const svg = d3.select('#note-graph') .append('svg') .attr('width', width) .attr('height', height); // Create force simulation const simulation = d3.forceSimulation(graphData.nodes) .force('link', d3.forceLink(graphData.links).id(d => d.id).distance(100)) .force('charge', d3.forceManyBody().strength(-300)) .force('center', d3.forceCenter(width / 2, height / 2)); // Create links const link = svg.append('g') .selectAll('line') .data(graphData.links) .enter().append('line') .style('stroke', d => d.type === 'child' ? '#999' : '#f00') .style('stroke-opacity', 0.6) .style('stroke-width', d => d.type === 'child' ? 2 : 1); // Create nodes const node = svg.append('g') .selectAll('circle') .data(graphData.nodes) .enter().append('circle') .attr('r', d => 10 - d.depth * 2) .style('fill', d => { const colors = { text: '#4CAF50', code: '#2196F3', file: '#FF9800', image: '#9C27B0' }; return colors[d.type] || '#666'; }) .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); // Add labels const label = svg.append('g') .selectAll('text') .data(graphData.nodes) .enter().append('text') .text(d => d.title) .style('font-size', '12px') .style('fill', '#333'); // Add tooltips node.append('title') .text(d => d.title); // Update positions on tick simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); node .attr('cx', d => d.x) .attr('cy', d => d.y); label .attr('x', d => d.x + 12) .attr('y', d => d.y + 4); }); // Drag functions function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } // Handle node clicks node.on('click', async (event, d) => { await api.activateNote(d.id); }); ``` ### 8. Markdown Preview Toggle Add live markdown preview for notes: ```javascript // Create preview pane const $previewPane = $(`
`); // Create toggle button const $toggleBtn = $(` `); // Add to note detail $('.note-detail-text').css('position', 'relative').append($previewPane, $toggleBtn); let previewVisible = false; let updateTimeout; // Load markdown library await api.requireLibrary('markdown-it'); const md = window.markdownit({ html: true, linkify: true, typographer: true, breaks: true }); // Toggle preview $toggleBtn.click(() => { previewVisible = !previewVisible; if (previewVisible) { $previewPane.show(); $('.note-detail-text .note-detail-editable').css('width', '50%'); $toggleBtn.html(' Hide'); updatePreview(); } else { $previewPane.hide(); $('.note-detail-text .note-detail-editable').css('width', '100%'); $toggleBtn.html(' Preview'); } }); // Update preview function async function updatePreview() { if (!previewVisible) return; const content = await api.getActiveContextTextEditor().getContent(); // Convert HTML to markdown first (simplified) let markdown = content .replace(/]*>(.*?)<\/h1>/g, '# $1\n') .replace(/]*>(.*?)<\/h2>/g, '## $1\n') .replace(/]*>(.*?)<\/h3>/g, '### $1\n') .replace(/]*>(.*?)<\/p>/g, '$1\n\n') .replace(/]*>(.*?)<\/strong>/g, '**$1**') .replace(/]*>(.*?)<\/b>/g, '**$1**') .replace(/]*>(.*?)<\/em>/g, '*$1*') .replace(/]*>(.*?)<\/i>/g, '*$1*') .replace(/]*>(.*?)<\/code>/g, '`$1`') .replace(/]*>/g, '') .replace(/<\/ul>/g, '\n') .replace(/]*>(.*?)<\/li>/g, '- $1\n') .replace(/]*>/g, '') .replace(/<\/ol>/g, '\n') .replace(/]*>(.*?)<\/li>/g, '1. $1\n') .replace(/]*>(.*?)<\/a>/g, '[$2]($1)') .replace(/]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/g, '![$2]($1)') .replace(/]*>/g, '\n') .replace(/<[^>]+>/g, ''); // Remove remaining HTML tags // Render markdown const html = md.render(markdown); $('#preview-content').html(html); // Syntax highlight code blocks $('#preview-content pre code').each(function() { if (window.hljs) { window.hljs.highlightElement(this); } }); } // Auto-update preview on content change api.bindGlobalShortcut('mod+s', async () => { if (previewVisible) { clearTimeout(updateTimeout); updateTimeout = setTimeout(updatePreview, 500); } }); // Update on note change api.onActiveContextNoteChange(async () => { if (previewVisible) { updatePreview(); } }); ``` ## Common Patterns ### 9. Template System Create and apply templates to new notes: ```javascript // Backend script to manage templates async function createFromTemplate(templateName, targetParentId, customData = {}) { // Find template const template = await api.getNoteWithLabel(`template:${templateName}`); if (!template) { throw new Error(`Template "${templateName}" not found`); } // Get template content and metadata const content = await template.getContent(); const attributes = await template.getAttributes(); // Process template variables let processedContent = content; const variables = { DATE: api.dayjs().format('YYYY-MM-DD'), TIME: api.dayjs().format('HH:mm:ss'), DATETIME: api.dayjs().format('YYYY-MM-DD HH:mm:ss'), USER: api.getAppInfo().username || 'User', ...customData }; for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`{{${key}}}`, 'g'); processedContent = processedContent.replace(regex, value); } // Create new note const parentNote = await api.getNote(targetParentId); const title = customData.title || `${templateName} - ${variables.DATE}`; const newNote = await api.createNote(parentNote, title, processedContent); // Copy attributes (except template label) for (const attr of attributes) { if (!attr.name.startsWith('template:')) { if (attr.type === 'label') { await newNote.setLabel(attr.name, attr.value); } else if (attr.type === 'relation') { await newNote.setRelation(attr.name, attr.value); } } } return newNote; } // Example: Meeting notes template const meetingTemplate = `

Meeting Notes - {{DATE}}

Date:{{DATE}}
Time:{{TIME}}
Attendees:{{ATTENDEES}}
Subject:{{SUBJECT}}

Agenda

  • {{AGENDA_ITEM_1}}
  • {{AGENDA_ITEM_2}}
  • {{AGENDA_ITEM_3}}

Discussion

Action Items

  • [ ]

Next Steps

`; // Create template note if it doesn't exist let templateNote = await api.getNoteWithLabel('template:meeting'); if (!templateNote) { templateNote = await api.createTextNote('root', 'Meeting Template', meetingTemplate); await templateNote.setLabel('template:meeting'); await templateNote.setLabel('hideFromTree'); // Hide template from tree } // Use template const meeting = await createFromTemplate('meeting', 'root', { title: 'Team Standup', ATTENDEES: 'John, Jane, Bob', SUBJECT: 'Weekly Status Update', AGENDA_ITEM_1: 'Review last week\'s tasks', AGENDA_ITEM_2: 'Current blockers', AGENDA_ITEM_3: 'Next week\'s priorities' }); api.log(`Created meeting note: ${meeting.title}`); ``` ### 10. Hierarchical Tag System Implement hierarchical tags with inheritance: ```javascript class HierarchicalTags { constructor() { this.tagHierarchy = {}; } async buildTagHierarchy() { // Find all tag definition notes const tagNotes = await api.searchForNotes('#tagDef'); for (const note of tagNotes) { const tagName = await note.getLabel('tagName'); const parentTag = await note.getLabel('parentTag'); if (tagName) { this.tagHierarchy[tagName.value] = { noteId: note.noteId, parent: parentTag ? parentTag.value : null, children: [] }; } } // Build children arrays for (const [tag, data] of Object.entries(this.tagHierarchy)) { if (data.parent && this.tagHierarchy[data.parent]) { this.tagHierarchy[data.parent].children.push(tag); } } return this.tagHierarchy; } async applyHierarchicalTag(noteId, tagName) { const note = await api.getNote(noteId); // Apply the tag await note.setLabel(tagName); // Apply all parent tags let currentTag = tagName; while (this.tagHierarchy[currentTag] && this.tagHierarchy[currentTag].parent) { const parentTag = this.tagHierarchy[currentTag].parent; await note.setLabel(parentTag); currentTag = parentTag; } } async getNotesWithTagHierarchy(tagName) { // Get all child tags const allTags = [tagName]; const queue = [tagName]; while (queue.length > 0) { const current = queue.shift(); if (this.tagHierarchy[current]) { for (const child of this.tagHierarchy[current].children) { allTags.push(child); queue.push(child); } } } // Search for notes with any of these tags const searchQuery = allTags.map(t => `#${t}`).join(' OR '); return await api.searchForNotes(searchQuery); } async createTagReport() { await this.buildTagHierarchy(); let report = '

Tag Hierarchy Report

\n'; // Build tree visualization const renderTree = (tag, level = 0) => { const indent = ' '.repeat(level * 4); let html = `${indent}• ${tag}`; const notes = api.searchForNotes(`#${tag}`); html += ` (${notes.length} notes)
\n`; if (this.tagHierarchy[tag] && this.tagHierarchy[tag].children.length > 0) { for (const child of this.tagHierarchy[tag].children) { html += renderTree(child, level + 1); } } return html; }; // Find root tags (no parent) const rootTags = Object.keys(this.tagHierarchy) .filter(tag => !this.tagHierarchy[tag].parent); for (const rootTag of rootTags) { report += renderTree(rootTag); } // Create or update report note let reportNote = await api.getNoteWithLabel('tagHierarchyReport'); if (!reportNote) { reportNote = await api.createTextNote('root', 'Tag Hierarchy Report', ''); await reportNote.setLabel('tagHierarchyReport'); } await reportNote.setContent(report); return report; } } // Usage const tagSystem = new HierarchicalTags(); // Define tag hierarchy const createTagDefinition = async (tagName, parentTag = null) => { let tagDef = await api.getNoteWithLabel(`tagDef:${tagName}`); if (!tagDef) { tagDef = await api.createTextNote('root', `Tag: ${tagName}`, `Tag definition for ${tagName}`); await tagDef.setLabel('tagDef'); await tagDef.setLabel(`tagDef:${tagName}`); await tagDef.setLabel('tagName', tagName); if (parentTag) { await tagDef.setLabel('parentTag', parentTag); } } return tagDef; }; // Create tag hierarchy await createTagDefinition('project'); await createTagDefinition('work', 'project'); await createTagDefinition('personal', 'project'); await createTagDefinition('development', 'work'); await createTagDefinition('documentation', 'work'); // Apply hierarchical tag await tagSystem.buildTagHierarchy(); await tagSystem.applyHierarchicalTag('someNoteId', 'documentation'); // This will also apply 'work' and 'project' tags // Get all notes in hierarchy const projectNotes = await tagSystem.getNotesWithTagHierarchy('project'); // Returns notes tagged with 'project', 'work', 'personal', 'development', or 'documentation' // Generate report await tagSystem.createTagReport(); ``` ## Integration with External Services ### 11. GitHub Integration Sync GitHub issues with notes: ```javascript // Requires axios library const axios = require('axios'); class GitHubSync { constructor(token, repo) { this.token = token; this.repo = repo; // format: "owner/repo" this.apiBase = 'https://api.github.com'; } async getIssues(state = 'open') { const response = await axios.get(`${this.apiBase}/repos/${this.repo}/issues`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' }, params: { state } }); return response.data; } async syncIssuesToNotes() { // Get or create GitHub folder let githubFolder = await api.getNoteWithLabel('githubSync'); if (!githubFolder) { githubFolder = await api.createTextNote('root', 'GitHub Issues', ''); await githubFolder.setLabel('githubSync'); } const issues = await this.getIssues(); const syncedNotes = []; for (const issue of issues) { // Check if issue note already exists let issueNote = await api.getNoteWithLabel(`github:issue:${issue.number}`); const content = `

${issue.title}

Issue #${issue.number}
State${issue.state}
Author${issue.user.login}
Created${api.dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}
Updated${api.dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}
Labels${issue.labels.map(l => l.name).join(', ')}

Description

${issue.body || 'No description'}

Links

`; if (!issueNote) { // Create new note issueNote = await api.createNote( githubFolder, `#${issue.number}: ${issue.title}`, content ); await issueNote.setLabel(`github:issue:${issue.number}`); } else { // Update existing note await issueNote.setContent(content); } // Set labels based on issue state and labels await issueNote.setLabel('githubIssue'); await issueNote.setLabel('state', issue.state); for (const label of issue.labels) { await issueNote.setLabel(`gh:${label.name}`); } syncedNotes.push({ noteId: issueNote.noteId, issueNumber: issue.number, title: issue.title }); } api.log(`Synced ${syncedNotes.length} GitHub issues`); return syncedNotes; } async createIssueFromNote(noteId) { const note = await api.getNote(noteId); const content = await note.getContent(); // Extract plain text from HTML const plainText = content.replace(/<[^>]*>/g, ''); const response = await axios.post( `${this.apiBase}/repos/${this.repo}/issues`, { title: note.title, body: plainText, labels: ['from-trilium'] }, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); // Link note to issue await note.setLabel(`github:issue:${response.data.number}`); await note.setLabel('githubIssue'); return response.data; } } // Usage const github = new GitHubSync( process.env.GITHUB_TOKEN || 'your-token', 'your-org/your-repo' ); // Sync issues to notes const synced = await github.syncIssuesToNotes(); // Create issue from current note // const issue = await github.createIssueFromNote('currentNoteId'); ``` ### 12. Email Integration Send notes via email: ```javascript const nodemailer = require('nodemailer'); class EmailIntegration { constructor(config) { this.transporter = nodemailer.createTransporter({ host: config.host || 'smtp.gmail.com', port: config.port || 587, secure: false, auth: { user: config.user, pass: config.pass } }); } async sendNoteAsEmail(noteId, to, options = {}) { const note = await api.getNote(noteId); const content = await note.getContent(); // Get attachments const attachments = await note.getAttachments(); const mailAttachments = []; for (const attachment of attachments) { const blob = await attachment.getBlob(); mailAttachments.push({ filename: attachment.title, content: blob.content, contentType: attachment.mime }); } // Convert note content to email-friendly HTML const emailHtml = ` ${content}

Sent from Trilium Notes on ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}

`; const mailOptions = { from: options.from || this.transporter.options.auth.user, to: to, subject: options.subject || note.title, html: emailHtml, attachments: mailAttachments }; const info = await this.transporter.sendMail(mailOptions); // Log email send await note.setLabel('emailSent', api.dayjs().format()); await note.setLabel('emailRecipient', to); api.log(`Email sent: ${info.messageId}`); return info; } async createEmailCampaign(templateNoteId, recipientListNoteId) { const template = await api.getNote(templateNoteId); const recipientNote = await api.getNote(recipientListNoteId); const recipientContent = await recipientNote.getContent(); // Parse recipient list (assume one email per line) const recipients = recipientContent .split('\n') .map(line => line.trim()) .filter(line => line && line.includes('@')); const results = []; for (const recipient of recipients) { try { const result = await this.sendNoteAsEmail( templateNoteId, recipient, { subject: template.title } ); results.push({ recipient, success: true, messageId: result.messageId }); // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { results.push({ recipient, success: false, error: error.message }); } } // Create campaign report const reportNote = await api.createTextNote( 'root', `Email Campaign Report - ${api.dayjs().format('YYYY-MM-DD')}`, `

Email Campaign Report

Template: ${template.title}

Sent: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}

Total Recipients: ${recipients.length}

Successful: ${results.filter(r => r.success).length}

Failed: ${results.filter(r => !r.success).length}

Results

${results.map(r => ` `).join('')}
RecipientStatusDetails
${r.recipient} ${r.success ? '✅ Sent' : '❌ Failed'} ${r.success ? r.messageId : r.error}
` ); await reportNote.setLabel('emailCampaignReport'); return results; } } // Usage const email = new EmailIntegration({ host: 'smtp.gmail.com', port: 587, user: 'your-email@gmail.com', pass: 'your-app-password' }); // Send single note // await email.sendNoteAsEmail('noteId', 'recipient@example.com'); // Send campaign // await email.createEmailCampaign('templateNoteId', 'recipientListNoteId'); ``` ## Best Practices ### Error Handling Always wrap scripts in try-catch blocks: ```javascript async function safeScriptExecution() { try { // Your script code here const result = await riskyOperation(); return { success: true, data: result }; } catch (error) { api.log(`Error in script: ${error.message}`, 'error'); // Create error report note const errorNote = await api.createTextNote( 'root', `Script Error - ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}`, `

Script Error

Error: ${error.message}

Stack:

${error.stack}

Script: ${api.currentNote.title}

` ); await errorNote.setLabel('scriptError'); return { success: false, error: error.message }; } } return await safeScriptExecution(); ``` ### Performance Optimization Use batch operations and caching: ```javascript class OptimizedNoteProcessor { constructor() { this.cache = new Map(); } async processNotes(noteIds) { // Batch fetch notes const notes = await Promise.all( noteIds.map(id => this.getCachedNote(id)) ); // Process in chunks to avoid memory issues const chunkSize = 100; const results = []; for (let i = 0; i < notes.length; i += chunkSize) { const chunk = notes.slice(i, i + chunkSize); const chunkResults = await Promise.all( chunk.map(note => this.processNote(note)) ); results.push(...chunkResults); // Allow other operations await new Promise(resolve => setTimeout(resolve, 10)); } return results; } async getCachedNote(noteId) { if (!this.cache.has(noteId)) { const note = await api.getNote(noteId); this.cache.set(noteId, note); } return this.cache.get(noteId); } async processNote(note) { // Process individual note return { noteId: note.noteId, processed: true }; } } ``` ### Script Organization Organize complex scripts with modules: ```javascript // Create a utility module note const utilsNote = await api.createCodeNote('root', 'Script Utils', ` module.exports = { formatDate: (date) => api.dayjs(date).format('YYYY-MM-DD'), sanitizeHtml: (html) => { return html .replace(/]*>.*?<\/script>/gi, '') .replace(/on\w+="[^"]*"/gi, ''); }, async createBackup(name) { await api.backupDatabase(name); api.log(\`Backup created: \${name}\`); } }; `, 'js'); await utilsNote.setLabel('scriptModule'); await utilsNote.setLabel('moduleName', 'utils'); // Use in another script const utils = await api.requireModule('utils'); const formattedDate = utils.formatDate(new Date()); ``` ### Testing Scripts Create test suites for your scripts: ```javascript class ScriptTester { constructor(scriptName) { this.scriptName = scriptName; this.tests = []; this.results = []; } test(description, testFn) { this.tests.push({ description, testFn }); } async run() { api.log(`Running tests for ${this.scriptName}`); for (const test of this.tests) { try { await test.testFn(); this.results.push({ description: test.description, passed: true }); api.log(`✅ ${test.description}`); } catch (error) { this.results.push({ description: test.description, passed: false, error: error.message }); api.log(`❌ ${test.description}: ${error.message}`); } } return this.generateReport(); } generateReport() { const passed = this.results.filter(r => r.passed).length; const failed = this.results.filter(r => !r.passed).length; return { script: this.scriptName, total: this.results.length, passed, failed, results: this.results }; } assert(condition, message) { if (!condition) { throw new Error(message || 'Assertion failed'); } } assertEquals(actual, expected, message) { if (actual !== expected) { throw new Error(message || `Expected ${expected}, got ${actual}`); } } } // Example test suite const tester = new ScriptTester('Note Utils'); tester.test('Create note', async () => { const note = await api.createTextNote('root', 'Test Note', 'Content'); tester.assert(note !== null, 'Note should be created'); tester.assertEquals(note.title, 'Test Note', 'Title should match'); // Clean up await note.delete(); }); tester.test('Search notes', async () => { const results = await api.searchForNotes('test'); tester.assert(Array.isArray(results), 'Results should be an array'); }); const report = await tester.run(); return report; ``` ## Conclusion The Script API provides powerful capabilities for automating and extending Trilium Notes. Key takeaways: 1. **Use Backend Scripts** for data processing, automation, and integrations 2. **Use Frontend Scripts** for UI enhancements and user interactions 3. **Always handle errors** gracefully and provide meaningful feedback 4. **Optimize performance** with caching and batch operations 5. **Organize complex scripts** into modules for reusability 6. **Test your scripts** to ensure reliability For more information: - [Backend Script API Reference](https://triliumnext.github.io/Docs/api/Backend_Script_API.html) - [Frontend Script API Reference](https://triliumnext.github.io/Docs/api/Frontend_Script_API.html) - [Custom Widget Development](./Custom%20Widget%20Development.md)