# Trilium Scripting System > **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [Script API Documentation](Script%20API/) ## Overview Trilium features a **powerful scripting system** that allows users to extend and customize the application without modifying source code. Scripts are written in JavaScript and can execute both in the **frontend (browser)** and **backend (Node.js)** contexts. ## Script Types ### Frontend Scripts **Location:** Attached to notes with `#run=frontendStartup` attribute **Execution Context:** Browser environment **Access:** - Trilium Frontend API - Browser APIs (DOM, localStorage, etc.) - Froca (frontend cache) - UI widgets - No direct file system access **Lifecycle:** - `frontendStartup` - Run once when Trilium loads - `frontendReload` - Run on every note context change **Example:** ```javascript // Attach to note with #run=frontendStartup const api = window.api // Add custom button to toolbar api.addButtonToToolbar({ title: 'My Button', icon: 'star', action: () => { api.showMessage('Hello from frontend!') } }) ``` ### Backend Scripts **Location:** Attached to notes with `#run=backendStartup` attribute **Execution Context:** Node.js server environment **Access:** - Trilium Backend API - Node.js APIs (fs, http, etc.) - Becca (backend cache) - Database (SQL) - External libraries (via require) **Lifecycle:** - `backendStartup` - Run once when server starts - Event handlers (custom events) **Example:** ```javascript // Attach to note with #run=backendStartup const api = require('@triliumnext/api') // Listen for note creation api.dayjs // Example: access dayjs library api.onNoteCreated((note) => { if (note.title.includes('TODO')) { note.setLabel('priority', 'high') } }) ``` ### Render Scripts **Location:** Attached to notes with `#customWidget` or similar attributes **Purpose:** Custom note rendering/widgets **Example:** ```javascript // Custom widget for a note class MyWidget extends api.NoteContextAwareWidget { doRender() { this.$widget = $('
') .text('Custom widget content') return this.$widget } } module.exports = MyWidget ``` ## Script API ### Frontend API **Location:** `apps/client/src/services/frontend_script_api.ts` **Global Access:** `window.api` **Key Methods:** ```typescript // Note Operations api.getNote(noteId) // Get note object api.getBranch(branchId) // Get branch object api.getActiveNote() // Currently displayed note api.openNote(noteId, activateNote) // Open note in UI // UI Operations api.showMessage(message) // Show toast notification api.showDialog() // Show modal dialog api.confirm(message) // Show confirmation dialog api.prompt(message, defaultValue) // Show input prompt // Tree Operations api.getTree() // Get note tree structure api.expandTree(noteId) // Expand tree branch api.collapseTree(noteId) // Collapse tree branch // Search api.searchForNotes(searchQuery) // Search notes api.searchForNote(searchQuery) // Get single note // Navigation api.openTabWithNote(noteId) // Open note in new tab api.closeActiveTab() // Close current tab api.activateNote(noteId) // Switch to note // Attributes api.getAttribute(noteId, type, name) // Get attribute api.getAttributes(noteId, type, name) // Get all matching attributes // Custom Widgets api.addButtonToToolbar(def) // Add toolbar button api.addCustomWidget(def) // Add custom widget // Events api.runOnNoteOpened(callback) // Note opened event api.runOnNoteContentChange(callback) // Content changed event // Utilities api.dayjs // Date/time library api.formatDate(date) // Format date api.log(message) // Console log ``` ### Backend API **Location:** `apps/server/src/services/backend_script_api.ts` **Access:** `require('@triliumnext/api')` or global `api` **Key Methods:** ```typescript // Note Operations api.getNote(noteId) // Get note from Becca api.getNoteWithContent(noteId) // Get note with content api.createNote(parentNoteId, title) // Create new note api.deleteNote(noteId) // Delete note // Branch Operations api.getBranch(branchId) // Get branch api.createBranch(noteId, parentNoteId) // Create branch (clone) // Attribute Operations api.getAttribute(noteId, type, name) // Get attribute api.createAttribute(noteId, type, name, value) // Create attribute // Database Access api.sql.getRow(query, params) // Execute SQL query (single row) api.sql.getRows(query, params) // Execute SQL query (multiple rows) api.sql.execute(query, params) // Execute SQL statement // Events api.onNoteCreated(callback) // Note created event api.onNoteUpdated(callback) // Note updated event api.onNoteDeleted(callback) // Note deleted event api.onAttributeCreated(callback) // Attribute created event // Search api.searchForNotes(searchQuery) // Search notes // Date/Time api.dayjs // Date/time library api.now() // Current date/time // Logging api.log(message) // Log message api.error(message) // Log error // External Communication api.axios // HTTP client library // Utilities api.backup.backupNow() // Trigger backup api.export.exportSubtree(noteId) // Export notes ``` ## Script Attributes ### Execute Attributes - `#run=frontendStartup` - Execute on frontend startup - `#run=backendStartup` - Execute on backend startup - `#run=hourly` - Execute every hour - `#run=daily` - Execute daily ### Widget Attributes - `#customWidget` - Custom note widget - `#widget` - Standard widget integration ### Other Attributes - `#disableVersioning` - Disable automatic versioning for this note - `#hideChildrenOverview` - Hide children in overview - `#iconClass` - Custom icon for note ## Entity Classes ### Frontend Entities **FNote** (`apps/client/src/entities/fnote.ts`) ```typescript class FNote { noteId: string title: string type: string mime: string // Relationships getParentNotes(): FNote[] getChildNotes(): FNote[] getBranches(): FBranch[] // Attributes getAttribute(type, name): FAttribute getAttributes(type?, name?): FAttribute[] hasLabel(name): boolean getLabelValue(name): string // Content getContent(): Promise // Navigation open(): void } ``` **FBranch** ```typescript class FBranch { branchId: string noteId: string parentNoteId: string prefix: string notePosition: number getNote(): FNote getParentNote(): FNote } ``` **FAttribute** ```typescript class FAttribute { attributeId: string noteId: string type: 'label' | 'relation' name: string value: string getNote(): FNote getTargetNote(): FNote // For relations } ``` ### Backend Entities **BNote** (`apps/server/src/becca/entities/bnote.ts`) ```typescript class BNote { noteId: string title: string type: string mime: string isProtected: boolean // Content getContent(): string | Buffer setContent(content: string | Buffer): void // Relationships getParentNotes(): BNote[] getChildNotes(): BNote[] getBranches(): BBranch[] // Attributes getAttribute(type, name): BAttribute getAttributes(type?, name?): BAttribute[] setLabel(name, value): BAttribute setRelation(name, targetNoteId): BAttribute hasLabel(name): boolean getLabelValue(name): string // Operations save(): void markAsDeleted(): void } ``` **BBranch** ```typescript class BBranch { branchId: string noteId: string parentNoteId: string prefix: string notePosition: number getNote(): BNote getParentNote(): BNote save(): void } ``` **BAttribute** ```typescript class BAttribute { attributeId: string noteId: string type: 'label' | 'relation' name: string value: string getNote(): BNote getTargetNote(): BNote // For relations save(): void } ``` ## Script Examples ### Frontend Examples **1. Custom Toolbar Button** ```javascript // #run=frontendStartup api.addButtonToToolbar({ title: 'Export to PDF', icon: 'file-export', action: async () => { const note = api.getActiveNote() if (note) { await api.runOnBackend('exportToPdf', [note.noteId]) api.showMessage('Export started') } } }) ``` **2. Auto-Save Reminder** ```javascript // #run=frontendStartup let saveTimer api.runOnNoteContentChange(() => { clearTimeout(saveTimer) saveTimer = setTimeout(() => { api.showMessage('Remember to save your work!') }, 300000) // 5 minutes }) ``` **3. Note Statistics Widget** ```javascript // #customWidget class StatsWidget extends api.NoteContextAwareWidget { doRender() { this.$widget = $('
') return this.$widget } async refreshWithNote(note) { const content = await note.getContent() const words = content.split(/\s+/).length const chars = content.length this.$widget.html(`
Words: ${words}
Characters: ${chars}
`) } } module.exports = StatsWidget ``` ### Backend Examples **1. Auto-Tagging on Note Creation** ```javascript // #run=backendStartup api.onNoteCreated((note) => { // Auto-tag TODO notes if (note.title.includes('TODO')) { note.setLabel('type', 'todo') note.setLabel('priority', 'normal') } // Auto-tag meeting notes by date if (note.title.match(/Meeting \d{4}-\d{2}-\d{2}/)) { note.setLabel('type', 'meeting') const dateMatch = note.title.match(/(\d{4}-\d{2}-\d{2})/) if (dateMatch) { note.setLabel('date', dateMatch[1]) } } }) ``` **2. Daily Backup Reminder** ```javascript // #run=daily const todayNote = api.getTodayNote() todayNote.setLabel('backupDone', 'false') // Create reminder note api.createNote(todayNote.noteId, '🔔 Backup Reminder', { content: 'Remember to verify today\'s backup!', type: 'text' }) ``` **3. External API Integration** ```javascript // #run=backendStartup api.onNoteCreated(async (note) => { // Sync new notes to external service if (note.hasLabel('sync-external')) { try { await api.axios.post('https://external-api.com/sync', { noteId: note.noteId, title: note.title, content: note.getContent() }) note.setLabel('lastSync', api.dayjs().format()) } catch (error) { api.log('Sync failed: ' + error.message) } } }) ``` **4. Database Cleanup** ```javascript // #run=weekly // Clean up old revisions const cutoffDate = api.dayjs().subtract(90, 'days').format() const oldRevisions = api.sql.getRows(` SELECT revisionId FROM revisions WHERE utcDateCreated < ? `, [cutoffDate]) api.log(`Deleting ${oldRevisions.length} old revisions`) for (const row of oldRevisions) { api.sql.execute('DELETE FROM revisions WHERE revisionId = ?', [row.revisionId]) } ``` ## Script Storage **Storage Location:** Scripts are stored as regular notes **Identifying Scripts:** - Have `#run` attribute or `#customWidget` attribute - Type is typically `code` with MIME `application/javascript` **Script Note Structure:** ``` 📁 Scripts (folder note) ├── 📜 Frontend Scripts │ ├── Custom Toolbar Button (#run=frontendStartup) │ └── Statistics Widget (#customWidget) └── 📜 Backend Scripts ├── Auto-Tagger (#run=backendStartup) └── Daily Backup (#run=daily) ``` ## Script Execution ### Frontend Execution **Timing:** 1. Trilium frontend loads 2. Froca cache initializes 3. Script notes with `#run=frontendStartup` are found 4. Scripts execute in dependency order **Isolation:** - Each script runs in separate context - Shared `window.api` object - Can access global window object ### Backend Execution **Timing:** 1. Server starts 2. Becca cache loads 3. Script notes with `#run=backendStartup` are found 4. Scripts execute in dependency order **Isolation:** - Each script is a separate module - Can require Node.js modules - Shared `api` global ### Error Handling **Frontend:** ```javascript try { // Script code } catch (error) { api.showError('Script error: ' + error.message) console.error(error) } ``` **Backend:** ```javascript try { // Script code } catch (error) { api.log('Script error: ' + error.message) console.error(error) } ``` ## Security Considerations ### Frontend Scripts **Risks:** - Can access all notes via Froca - Can manipulate DOM - Can make API calls - Limited by browser security model **Mitigations:** - User must trust scripts they add - Scripts run with user privileges - No access to file system ### Backend Scripts **Risks:** - Full Node.js access - Can execute system commands - Can access file system - Can make network requests **Mitigations:** - Scripts are user-created (trusted) - Single-user model (no privilege escalation) - Review scripts before adding `#run` attribute ### Best Practices 1. **Review script code** before adding execution attributes 2. **Use specific attributes** rather than wildcard searches 3. **Avoid eval()** and dynamic code execution 4. **Validate inputs** in scripts 5. **Handle errors** gracefully 6. **Log important actions** for audit trail ## Performance Considerations ### Optimization Tips **1. Cache Results:** ```javascript // Bad: Re-query on every call function getConfig() { return api.getNote('config').getContent() } // Good: Cache the result let cachedConfig function getConfig() { if (!cachedConfig) { cachedConfig = api.getNote('config').getContent() } return cachedConfig } ``` **2. Use Efficient Queries:** ```javascript // Bad: Load all notes and filter const todos = api.searchForNotes('#type=todo') // Good: Use specific search const todos = api.searchForNotes('#type=todo #status=pending') ``` **3. Batch Operations:** ```javascript // Bad: Save after each change notes.forEach(note => { note.title = 'Updated' note.save() }) // Good: Batch changes notes.forEach(note => { note.title = 'Updated' }) // Save happens in batch ``` **4. Debounce Event Handlers:** ```javascript let timeout api.runOnNoteContentChange(() => { clearTimeout(timeout) timeout = setTimeout(() => { // Process change }, 500) }) ``` ## Debugging Scripts ### Frontend Debugging **Browser DevTools:** ```javascript console.log('Debug info:', data) debugger // Breakpoint ``` **Trilium Log:** ```javascript api.log('Script executed') ``` ### Backend Debugging **Console Output:** ```javascript console.log('Backend debug:', data) api.log('Script log message') ``` **Inspect Becca:** ```javascript api.log('Note count:', Object.keys(api.becca.notes).length) ``` ## Advanced Topics ### Custom Note Types Scripts can implement custom note type handlers: ```javascript // Register custom type api.registerNoteType({ type: 'mytype', mime: 'application/x-mytype', renderNote: (note) => { // Custom rendering } }) ``` ### External Libraries **Frontend:** ```javascript // Load external library const myLib = await import('https://cdn.example.com/lib.js') ``` **Backend:** ```javascript // Use Node.js require const fs = require('fs') const axios = require('axios') ``` ### State Persistence **Frontend:** ```javascript // Use localStorage localStorage.setItem('myScript:data', JSON.stringify(data)) const data = JSON.parse(localStorage.getItem('myScript:data')) ``` **Backend:** ```javascript // Store in special note const stateNote = api.getNote('script-state-note') stateNote.setContent(JSON.stringify(data)) const data = JSON.parse(stateNote.getContent()) ``` --- **See Also:** - [Script API Documentation](Script%20API/) - Complete API reference - [Advanced Showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) - Example scripts - [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture