apps/server/src/assets/llm/skills/frontend_scripting.md
Frontend scripts run in the browser. They can manipulate the UI, navigate notes, show dialogs, and create custom widgets.
IMPORTANT: Always prefer Preact JSX widgets over legacy jQuery widgets. Use JSX code notes with import/export syntax.
CRITICAL: In JSX notes, always use top-level import statements (e.g. import { useState } from "trilium:preact"). NEVER use dynamic await import() for Preact imports — this will break hooks and components. Dynamic imports are not needed because JSX notes natively support ES module import/export syntax.
#widget label for widgets, or #run=frontendStartup for auto-run scripts.#run=mobileStartup instead.| Type | Language | Required attribute |
|---|---|---|
| Custom widget | JSX (preferred) | #widget |
| Regular script | JS frontend | #run=frontendStartup (optional) |
| Render note | JSX | None (used via ~renderNote relation) |
import { defineWidget } from "trilium:preact";
import { useState } from "trilium:preact";
export default defineWidget({
parent: "center-pane",
position: 10,
render: () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</button>
</div>
);
}
});
import { defineWidget, useNoteContext, useNoteProperty } from "trilium:preact";
export default defineWidget({
parent: "note-detail-pane",
position: 10,
render: () => {
const { note } = useNoteContext();
const title = useNoteProperty(note, "title");
return <span>Current note: {title}</span>;
}
});
import { defineWidget, RightPanelWidget, useState, useEffect } from "trilium:preact";
export default defineWidget({
parent: "right-pane",
position: 1,
render() {
const [time, setTime] = useState();
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(interval);
});
return (
<RightPanelWidget id="my-clock" title="Clock">
<p>The time is: {time}</p>
</RightPanelWidget>
);
}
});
parent values)| Value | Description | Notes |
|---|---|---|
left-pane | Alongside the note tree | |
center-pane | Content area, spanning all splits | |
note-detail-pane | Inside a note, split-aware | Use useNoteContext() hook |
right-pane | Right sidebar section | Wrap in <RightPanelWidget> |
// API methods
import { showMessage, showError, getNote, searchForNotes, activateNote,
runOnBackend, getActiveContextNote } from "trilium:api";
// Hooks and components
import { defineWidget, defineLauncherWidget,
useState, useEffect, useCallback, useMemo, useRef,
useNoteContext, useActiveNoteContext, useNoteProperty,
RightPanelWidget } from "trilium:preact";
// Built-in UI components
import { ActionButton, Button, LinkButton, Modal,
NoteAutocomplete, FormTextBox, FormToggle, FormCheckbox,
FormDropdownList, FormGroup, FormText, FormTextArea,
Icon, LoadingSpinner, Slider, Collapsible } from "trilium:preact";
useNoteContext() - returns { note } for the current note context (use in note-detail-pane)useActiveNoteContext() - returns { note, noteId } for the active note (works from any widget location)useNoteProperty(note, propName) - reactively watches a note property (e.g. "title", "type")For rendering custom content inside a note:
~renderNote relation pointing to the child JSX note.IMPORTANT: Always create the JSX code note as a child of the render note, not as a sibling or at the root. This keeps them organized together.
export default function MyRenderNote() {
return (
<>
<h1>Custom rendered content</h1>
<p>This appears inside the note.</p>
</>
);
}
In JSX, use import { method } from "trilium:api". In JS frontend, use the api global.
activateNote(notePath) - navigate to a noteactivateNewNote(notePath) - navigate and wait for syncopenTabWithNote(notePath, activate?) - open in new tabopenSplitWithNote(notePath, activate?) - open in new splitgetActiveContextNote() - get currently active notegetActiveContextNotePath() - get path of active notesetHoistedNoteId(noteId) - hoist/unhoist notegetNote(noteId) - get note by IDgetNotes(noteIds) - bulk fetch notessearchForNotes(searchString) - search with full query syntaxsearchForNote(searchString) - search returning first resultgetTodayNote() - get/create today's notegetDayNote(date) / getWeekNote(date) / getMonthNote(month) / getYearNote(year)getActiveContextTextEditor() - get CKEditor instancegetActiveContextCodeEditor() - get CodeMirror instanceaddTextToActiveContextEditor(text) - insert text into active editorshowMessage(msg) - info toastshowError(msg) - error toastshowConfirmDialog(msg) - confirm dialog (returns boolean)showPromptDialog(msg) - prompt dialog (returns user input)runOnBackend(func, params) - execute a function on the backendtriggerCommand(name, data) - trigger a commandbindGlobalShortcut(shortcut, handler, namespace?) - add keyboard shortcutformatDateISO(date) - format as YYYY-MM-DDrandomString(length) - generate random stringdayjs - day.js librarylog(message) - log to script log paneAvailable via getNote(), getActiveContextNote(), useNoteContext(), etc.
note.noteId, note.title, note.type, note.mimenote.isProtected, note.isArchivednote.getContent() - get note contentnote.getJsonContent() - parse content as JSONnote.getParentNotes() / note.getChildNotes()note.hasChildren(), note.getSubtreeNoteIds()note.getAttributes(type?, name?) - all attributes (including inherited)note.getOwnedAttributes(type?, name?) - only owned attributesnote.hasAttribute(type, name) - check for attributeOnly use legacy widgets if you specifically need jQuery or cannot use JSX.
// Language: JS frontend, Label: #widget
class MyWidget extends api.BasicWidget {
get position() { return 1; }
get parentWidget() { return "center-pane"; }
doRender() {
this.$widget = $("<div>");
this.$widget.append($("<button>Click me</button>")
.on("click", () => api.showMessage("Hello!")));
return this.$widget;
}
}
module.exports = new MyWidget();
Key differences from Preact:
api. global instead of importsget parentWidget() instead of parent fieldmodule.exports = new MyWidget() (instance) for most widgetsmodule.exports = MyWidget (class, no new) for note-detail-paneapi.RightPanelWidget, override doRenderBody() instead of doRender()For JSX, use import/export syntax between notes. For JS frontend, use module.exports and function parameters matching child note titles.