Files
Trilium/docs/SCRIPTING.md
2025-11-02 21:59:29 +00:00

16 KiB
Vendored

Trilium Scripting System

Related: ARCHITECTURE.md | Script API Documentation

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:

// 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:

// 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:

// Custom widget for a note
class MyWidget extends api.NoteContextAwareWidget {
    doRender() {
        this.$widget = $('<div>')
            .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:

// 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:

// 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)

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<string>
    
    // Navigation
    open(): void
}

FBranch

class FBranch {
    branchId: string
    noteId: string
    parentNoteId: string
    prefix: string
    notePosition: number
    
    getNote(): FNote
    getParentNote(): FNote
}

FAttribute

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)

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

class BBranch {
    branchId: string
    noteId: string
    parentNoteId: string
    prefix: string
    notePosition: number
    
    getNote(): BNote
    getParentNote(): BNote
    save(): void
}

BAttribute

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

// #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

// #run=frontendStartup
let saveTimer
api.runOnNoteContentChange(() => {
    clearTimeout(saveTimer)
    saveTimer = setTimeout(() => {
        api.showMessage('Remember to save your work!')
    }, 300000) // 5 minutes
})

3. Note Statistics Widget

// #customWidget
class StatsWidget extends api.NoteContextAwareWidget {
    doRender() {
        this.$widget = $('<div class="stats-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(`
            <div>Words: ${words}</div>
            <div>Characters: ${chars}</div>
        `)
    }
}

module.exports = StatsWidget

Backend Examples

1. Auto-Tagging on Note Creation

// #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

// #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

// #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

// #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:

try {
    // Script code
} catch (error) {
    api.showError('Script error: ' + error.message)
    console.error(error)
}

Backend:

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:

// 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:

// 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:

// 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:

let timeout
api.runOnNoteContentChange(() => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
        // Process change
    }, 500)
})

Debugging Scripts

Frontend Debugging

Browser DevTools:

console.log('Debug info:', data)
debugger  // Breakpoint

Trilium Log:

api.log('Script executed')

Backend Debugging

Console Output:

console.log('Backend debug:', data)
api.log('Script log message')

Inspect Becca:

api.log('Note count:', Object.keys(api.becca.notes).length)

Advanced Topics

Custom Note Types

Scripts can implement custom note type handlers:

// Register custom type
api.registerNoteType({
    type: 'mytype',
    mime: 'application/x-mytype',
    renderNote: (note) => {
        // Custom rendering
    }
})

External Libraries

Frontend:

// Load external library
const myLib = await import('https://cdn.example.com/lib.js')

Backend:

// Use Node.js require
const fs = require('fs')
const axios = require('axios')

State Persistence

Frontend:

// Use localStorage
localStorage.setItem('myScript:data', JSON.stringify(data))
const data = JSON.parse(localStorage.getItem('myScript:data'))

Backend:

// Store in special note
const stateNote = api.getNote('script-state-note')
stateNote.setContent(JSON.stringify(data))

const data = JSON.parse(stateNote.getContent())

See Also: