Skip to content

SDK Reference

The Plugin SDK provides everything your plugin needs to interact with Treeline. It’s passed to your views via props when they mount.

import type { PluginSDK } from "@treeline-money/plugin-sdk";
interface Props {
sdk: PluginSDK;
}
let { sdk }: Props = $props();

Execute a read-only SQL query and return objects keyed by column name. This is the recommended method for most queries.

sql<T = Record<string, unknown>>(
sql: string,
params?: QueryParam[]
): Promise<T[]>

Parameters:

  • sql - SQL SELECT query with ? placeholders for parameters
  • params - Optional array of values to bind to placeholders

Returns: Array of row objects with column names as keys

Example:

// Simple query
const accounts = await sdk.sql<Account>("SELECT * FROM accounts");
// Parameterized query (recommended for user input)
const transactions = await sdk.sql<Transaction>(
"SELECT * FROM transactions WHERE amount > ? AND posted_date > ?",
[100, "2024-01-01"]
);
// With type parameter
interface Transaction {
transaction_id: string;
amount: number;
description: string;
posted_date: string;
}
const results = await sdk.sql<Transaction>(
"SELECT * FROM transactions LIMIT 10"
);
console.log(results[0].amount); // access by column name

Query Parameter Types:

type QueryParam = string | number | boolean | null | string[] | number[];

Execute a read-only SQL query and return raw row arrays. Use sdk.sql() instead if you want objects keyed by column name.

query<T = unknown[]>(
sql: string,
params?: QueryParam[]
): Promise<T[]>

Parameters:

  • sql - SQL SELECT query with ? placeholders for parameters
  • params - Optional array of values to bind to placeholders

Returns: Array of raw row arrays (values in column order)

Example:

const rows = await sdk.query("SELECT COUNT(*) FROM transactions");
console.log(rows[0][0]); // access by index

Execute a write SQL query (INSERT, UPDATE, DELETE, CREATE, DROP).

execute(
sql: string,
params?: QueryParam[]
): Promise<{ rowsAffected: number }>

Parameters:

  • sql - SQL write query with ? placeholders
  • params - Optional array of values to bind

Returns: Object with rowsAffected count

Example:

const schema = sdk.getSchemaName();
// Insert with parameters
await sdk.execute(
`INSERT INTO ${schema}.items (id, name, amount) VALUES (?, ?, ?)`,
[crypto.randomUUID(), "New Item", 100]
);
// Update
const result = await sdk.execute(
`UPDATE ${schema}.items SET amount = ? WHERE id = ?`,
[150, itemId]
);
console.log(`Updated ${result.rowsAffected} rows`);
// Delete
await sdk.execute(
`DELETE FROM ${schema}.items WHERE id = ?`,
[itemId]
);

Get the schema name for this plugin’s tables.

getSchemaName(): string

Returns: The plugin’s schema name (e.g., "plugin_goals")

Example:

const schema = sdk.getSchemaName(); // "plugin_my_plugin"
// Use in queries
await sdk.execute(`
CREATE TABLE IF NOT EXISTS ${schema}.items (
id VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL
)
`);

Show toast notifications to the user.

toast: {
show: (message: string, description?: string) => void;
success: (message: string, description?: string) => void;
error: (message: string, description?: string) => void;
warning: (message: string, description?: string) => void;
info: (message: string, description?: string) => void;
}

Example:

// Success message
sdk.toast.success("Saved!", "Your changes have been saved");
// Error with details
sdk.toast.error("Failed to save", error.message);
// Warning
sdk.toast.warning("Heads up", "This action cannot be undone");
// Info
sdk.toast.info("Tip", "Press Cmd+K to open the command palette");
// Generic (same as info)
sdk.toast.show("Hello", "This is a notification");

Navigate to another view in the application.

openView(viewId: string, props?: Record<string, unknown>): void

Parameters:

  • viewId - The view ID to open
  • props - Optional props to pass to the view

Example:

// Open a built-in view
sdk.openView("accounts");
// Open with props
sdk.openView("query", {
initialQuery: "SELECT * FROM transactions LIMIT 10"
});
// Open another plugin's view
sdk.openView("budget-view");

Subscribe to data refresh events. Called when data changes (sync, import, etc.).

onDataRefresh(callback: () => void): () => void

Parameters:

  • callback - Function to call when data is refreshed

Returns: Unsubscribe function

Example:

import { onMount, onDestroy } from "svelte";
let unsubscribe: (() => void) | null = null;
onMount(() => {
// Subscribe to data changes
unsubscribe = sdk.onDataRefresh(() => {
loadData(); // Reload your data
});
});
onDestroy(() => {
// Clean up subscription
if (unsubscribe) unsubscribe();
});

Notify other views that data has changed. Call this after modifying data.

emitDataRefresh(): void

Example:

async function saveItem(item: Item) {
await sdk.execute(
`INSERT INTO ${sdk.getSchemaName()}.items (id, name) VALUES (?, ?)`,
[item.id, item.name]
);
// Notify other views
sdk.emitDataRefresh();
sdk.toast.success("Saved!");
}

Update the badge count shown on this plugin’s sidebar item.

updateBadge(count: number | undefined): void

Parameters:

  • count - Badge count to display (0 or undefined to hide)

Example:

// Show a badge
sdk.updateBadge(5);
// Hide the badge
sdk.updateBadge(0);
sdk.updateBadge(undefined);
// Update based on data
const pending = await sdk.sql<{ count: number }>(
`SELECT COUNT(*) as count FROM ${sdk.getSchemaName()}.tasks WHERE done = false`
);
sdk.updateBadge(pending[0]?.count || 0);

Theme utilities for adapting to light/dark mode.

theme: {
current: () => "light" | "dark";
subscribe: (callback: (theme: string) => void) => () => void;
}

Get the current theme.

const theme = sdk.theme.current(); // "light" or "dark"

Subscribe to theme changes.

const unsubscribe = sdk.theme.subscribe((theme) => {
console.log("Theme changed to:", theme);
});
// Later, unsubscribe
unsubscribe();

Full Example:

<script lang="ts">
import { onMount } from "svelte";
let theme = $state<"light" | "dark">("light");
onMount(() => {
theme = sdk.theme.current();
sdk.theme.subscribe((newTheme) => {
theme = newTheme as "light" | "dark";
});
});
</script>
<div class:dark={theme === "dark"}>
<!-- Content adapts to theme -->
</div>

Currency formatting utilities that respect the user’s currency preference.

currency: {
format: (amount: number, currency?: string) => string;
formatCompact: (amount: number, currency?: string) => string;
formatAmount: (amount: number) => string;
getSymbol: (currency: string) => string;
getUserCurrency: () => string;
supportedCurrencies: string[];
}

Format an amount with currency symbol.

sdk.currency.format(1234.56); // "$1,234.56" (user's currency)
sdk.currency.format(1234.56, "EUR"); // "EUR 1,234.56"
sdk.currency.format(-50); // "-$50.00"

Format large amounts compactly.

sdk.currency.formatCompact(1234567); // "$1.2M"
sdk.currency.formatCompact(50000); // "$50K"
sdk.currency.formatCompact(999); // "$999"
sdk.currency.formatCompact(1234567, "GBP"); // "GBP 1.2M"

Format just the number without currency symbol.

sdk.currency.formatAmount(1234.56); // "1,234.56"
sdk.currency.formatAmount(-99.99); // "-99.99"

Get the symbol for a currency code.

sdk.currency.getSymbol("USD"); // "$"
sdk.currency.getSymbol("EUR"); // "EUR"
sdk.currency.getSymbol("GBP"); // "GBP"
sdk.currency.getSymbol("JPY"); // "JPY"

Get the user’s configured currency code.

const currency = sdk.currency.getUserCurrency(); // "USD"

List of supported currency codes.

sdk.currency.supportedCurrencies; // ["USD", "EUR", "GBP", "JPY", ...]

Persistent settings scoped to your plugin. Survives app restarts.

settings: {
get: <T extends Record<string, unknown>>() => Promise<T>;
set: <T extends Record<string, unknown>>(settings: T) => Promise<void>;
}

Example:

interface MySettings {
showCompleted: boolean;
sortOrder: "asc" | "desc";
}
// Load settings
const settings = await sdk.settings.get<MySettings>();
console.log(settings.showCompleted); // true or false
// Save settings
await sdk.settings.set<MySettings>({
showCompleted: true,
sortOrder: "desc",
});

Ephemeral state scoped to your plugin. Cleared on app restart.

state: {
read: <T>() => Promise<T | null>;
write: <T>(state: T) => Promise<void>;
}

Example:

interface MyState {
selectedId: string | null;
scrollPosition: number;
}
// Read state
const state = await sdk.state.read<MyState>();
// Write state
await sdk.state.write<MyState>({
selectedId: "item-123",
scrollPosition: 500,
});

When to use which:

Use CaseTool
User preferencessdk.settings
UI configurationsdk.settings
Selected items (temporary)sdk.state
Scroll positionsdk.state
Form draftssdk.state

Platform-aware modifier key string.

modKey: "Cmd" | "Ctrl"

Example:

sdk.modKey; // "Cmd" on Mac, "Ctrl" on Windows/Linux
// In UI
<span>Press {sdk.modKey}+K to search</span>

Format a keyboard shortcut for display.

formatShortcut(shortcut: string): string

Parameters:

  • shortcut - Shortcut string using mod for platform modifier

Returns: Formatted shortcut for current platform

Example:

sdk.formatShortcut("mod+p"); // "Cmd+P" on Mac, "Ctrl+P" on Windows
sdk.formatShortcut("mod+shift+k"); // "Cmd+Shift+K" on Mac
sdk.formatShortcut("mod+enter"); // "Cmd+Enter" on Mac

These types are used when registering views, commands, and sidebar items in activate().

interface ViewDefinition {
id: string; // Unique view ID
name: string; // Display name (shown in tab)
icon: string; // Lucide icon name
mount: (target: HTMLElement, props: { sdk: PluginSDK }) => () => void;
allowMultiple?: boolean; // Can multiple instances be open?
}
interface SidebarItem {
id: string; // Unique ID
label: string; // Display label
icon: string; // Lucide icon name
sectionId: string; // Section this belongs to ("main")
viewId: string; // View to open when clicked
shortcut?: string; // Keyboard shortcut hint
order?: number; // Sort order within section
}
interface Command {
id: string; // Unique command ID
name: string; // Display name
description?: string; // Optional description
category?: string; // Category for grouping
shortcut?: string; // Keyboard shortcut
execute: () => void | Promise<void>; // Function to execute
}
interface PluginMigration {
version: number; // Unique version number (positive integer)
name: string; // Human-readable name
up: string; // SQL to execute
}

Here’s a complete example using multiple SDK features:

<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { PluginSDK } from "@treeline-money/plugin-sdk";
interface Props {
sdk: PluginSDK;
}
let { sdk }: Props = $props();
interface Item {
id: string;
name: string;
amount: number;
}
let items = $state<Item[]>([]);
let isLoading = $state(true);
let unsubscribe: (() => void) | null = null;
const schema = sdk.getSchemaName();
onMount(async () => {
// Subscribe to data refresh
unsubscribe = sdk.onDataRefresh(() => loadData());
// Load initial data
await loadData();
});
onDestroy(() => {
if (unsubscribe) unsubscribe();
});
async function loadData() {
isLoading = true;
try {
items = await sdk.sql<Item>(
`SELECT * FROM ${schema}.items ORDER BY name`
);
sdk.updateBadge(items.length);
} catch (e) {
sdk.toast.error("Failed to load", e instanceof Error ? e.message : String(e));
} finally {
isLoading = false;
}
}
async function addItem() {
try {
await sdk.execute(
`INSERT INTO ${schema}.items (id, name, amount) VALUES (?, ?, ?)`,
[crypto.randomUUID(), "New Item", 0]
);
sdk.toast.success("Added!");
sdk.emitDataRefresh();
await loadData();
} catch (e) {
sdk.toast.error("Failed to add", e instanceof Error ? e.message : String(e));
}
}
async function deleteItem(id: string) {
try {
await sdk.execute(
`DELETE FROM ${schema}.items WHERE id = ?`,
[id]
);
sdk.emitDataRefresh();
await loadData();
} catch (e) {
sdk.toast.error("Failed to delete", e instanceof Error ? e.message : String(e));
}
}
</script>
<div class="tl-view">
<div class="tl-header">
<div class="tl-header-left">
<h1 class="tl-title">My Items</h1>
<p class="tl-subtitle">Press {sdk.formatShortcut("mod+n")} to add</p>
</div>
<div class="tl-header-right">
<button class="tl-btn tl-btn-primary" onclick={addItem}>Add Item</button>
</div>
</div>
<div class="tl-content">
{#if isLoading}
<div class="tl-loading"><div class="tl-spinner"></div></div>
{:else if items.length === 0}
<div class="tl-empty">
<p class="tl-empty-title">No items yet</p>
<p class="tl-empty-message">Click "Add Item" to get started</p>
</div>
{:else}
<div class="tl-table-container">
<table class="tl-table">
<thead>
<tr>
<th>Name</th>
<th style:text-align="right">Amount</th>
<th></th>
</tr>
</thead>
<tbody>
{#each items as item}
<tr>
<td>{item.name}</td>
<td class="tl-cell-number">{sdk.currency.format(item.amount)}</td>
<td class="tl-cell-actions">
<button class="tl-btn tl-btn-danger" onclick={() => deleteItem(item.id)}>Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>