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 loadsfrontendReload- 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
#runattribute or#customWidgetattribute - Type is typically
codewith MIMEapplication/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:
- Trilium frontend loads
- Froca cache initializes
- Script notes with
#run=frontendStartupare found - Scripts execute in dependency order
Isolation:
- Each script runs in separate context
- Shared
window.apiobject - Can access global window object
Backend Execution
Timing:
- Server starts
- Becca cache loads
- Script notes with
#run=backendStartupare found - Scripts execute in dependency order
Isolation:
- Each script is a separate module
- Can require Node.js modules
- Shared
apiglobal
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
#runattribute
Best Practices
- Review script code before adding execution attributes
- Use specific attributes rather than wildcard searches
- Avoid eval() and dynamic code execution
- Validate inputs in scripts
- Handle errors gracefully
- 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:
- Script API Documentation - Complete API reference
- Advanced Showcases - Example scripts
- ARCHITECTURE.md - Overall architecture