Backend API Reference
The backend runs in the host Bun process. It’s optional — only needed when your instrument requires server-side logic.
defineBackend
Section titled “defineBackend”Creates a backend definition. Must be the default export of your backend entry file.
import { defineBackend } from "tango-api/backend";
export default defineBackend({ kind: "tango.instrument.backend.v2", actions: { /* ... */ }, onStart: async (ctx) => { /* ... */ }, onStop: async () => { /* ... */ },});InstrumentBackendDefinition
Section titled “InstrumentBackendDefinition”type InstrumentBackendDefinition = { kind: "tango.instrument.backend.v2"; actions: Record<string, InstrumentBackendAction<any, any>>; onStart?: (ctx: InstrumentBackendContext) => Promise<void> | void; onStop?: () => Promise<void> | void; onBackgroundRefresh?: (ctx: InstrumentBackgroundRefreshContext) => Promise<void> | void;};Must be "tango.instrument.backend.v2". Required for version negotiation.
actions
Section titled “actions”A map of action name → action definition. Each action is callable from the frontend via api.actions.call(name, input) or useInstrumentAction(name).
onStart(ctx)
Section titled “onStart(ctx)”Called when the instrument backend loads. Use it for initialization (creating tables, setting up state, etc.).
onStop()
Section titled “onStop()”Called when the instrument unloads or is suspended (user navigated away). Use it for cleanup.
onBackgroundRefresh(ctx)
Section titled “onBackgroundRefresh(ctx)”Called periodically while the instrument is suspended, if backgroundRefresh is enabled in the manifest. Receives a restricted context — only storage, settings, emit, and logger are available. See Background Refresh for full details.
InstrumentBackendAction
Section titled “InstrumentBackendAction”type InstrumentBackendAction<I = unknown, O = unknown> = { input?: ActionSchema; output?: ActionSchema; handler: (ctx: InstrumentBackendContext, input: I) => Promise<O> | O;};Optional JSON schema describing the expected input. Currently used for documentation; future versions will validate inputs against this schema.
output
Section titled “output”Optional JSON schema describing the return value.
handler(ctx, input)
Section titled “handler(ctx, input)”The function that runs when the action is called. Receives the backend context and the caller’s input. Can be sync or async.
InstrumentBackendContext
Section titled “InstrumentBackendContext”type InstrumentBackendContext = { instrumentId: string; permissions: InstrumentPermission[]; emit: (event: Omit<InstrumentEvent, "instrumentId">) => void; host: InstrumentBackendHostAPI; task: { (label: string): TaskHandle; <T>(label: string, fn: () => Promise<T> | T): Promise<T>; };};
type TaskHandle = { done(): void; fail(error?: string): void;};instrumentId
Section titled “instrumentId”The instrument’s unique ID from the manifest.
permissions
Section titled “permissions”The permissions granted to this instrument.
emit(event)
Section titled “emit(event)”Sends a custom event to the frontend. The instrumentId is added automatically.
ctx.emit({ event: "processing.done", payload: { resultCount: 42 },});Access to the same host APIs as the frontend:
type InstrumentBackendHostAPI = { storage: StorageAPI; sessions: SessionsAPI; connectors: ConnectorsAPI; stages: StageAPI; events: HostEventsAPI; settings: InstrumentSettingsAPI;};All methods work identically to the frontend versions. See Frontend API Reference for details on each.
task(label) / task(label, fn)
Section titled “task(label) / task(label, fn)”Registers a long-running task that prevents backend suspension while it runs. When the user navigates away, suspension is deferred until all active tasks complete.
Handle-based — for fire-and-forget or streaming flows:
const handle = ctx.task("Reviewing PR #42");try { const review = await runAgentReview(prUrl); await ctx.host.storage.setProperty("review", review); ctx.emit({ event: "review.done", payload: { review } }); handle.done();} catch (err) { handle.fail(err.message);}Callback-based (recommended) — auto-completes on success, auto-fails on error:
const result = await ctx.task("Reviewing PR #42", async () => { const review = await runAgentReview(prUrl); await ctx.host.storage.setProperty("review", review); ctx.emit({ event: "review.done", payload: { review } }); return review;});Behavior:
- Multiple tasks can run concurrently. Suspension is deferred until all complete.
- Tasks have a 5-minute hard timeout. Exceeding it auto-completes with a warning log.
done()andfail()are idempotent — safe to call multiple times.- If the user navigates back while tasks run, the deferred suspension is cancelled.
- When an instrument is disabled or removed, all tasks are force-completed.
When to use: Agent reviews, content generation, session queries — any operation taking more than a few seconds that should survive navigation.
When NOT to use: For backends that need to stay alive permanently, use backgroundRefresh: { mode: "keep-alive" } instead.
InstrumentBackgroundRefreshContext
Section titled “InstrumentBackgroundRefreshContext”A restricted version of the backend context passed to onBackgroundRefresh. Limits available APIs to prevent heavy workloads during background refresh.
type InstrumentBackgroundRefreshContext = { instrumentId: string; permissions: InstrumentPermission[]; emit: (event: Omit<InstrumentEvent, "instrumentId">) => void; logger: LoggerAPI; host: { storage: StorageAPI; settings: InstrumentSettingsAPI; };};| API | Available | Notes |
|---|---|---|
storage | Yes | Full read/write access |
settings | Yes | Read/write instrument settings |
emit | Yes | Send events |
logger | Yes | Log messages at any level |
sessions | No | Background refresh should not spawn Claude sessions |
connectors | No | Too heavy for periodic background work |
stages | No | Not needed for data refresh |
events.subscribe | No | Subscriptions are cleared on suspend |
See Background Refresh for lifecycle flow, concurrency rules, and common patterns.
ActionSchema
Section titled “ActionSchema”Used for input and output type declarations on actions:
type ActionSchema = | { type: "any" } | { type: "null" } | { type: "string" } | { type: "number" } | { type: "boolean" } | { type: "array"; items?: ActionSchema } | { type: "object"; properties?: Record<string, ActionSchema>; required?: string[]; additionalProperties?: boolean; };Examples
Section titled “Examples”Simple string input:
input: { type: "string" }Object with required fields:
input: { type: "object", properties: { name: { type: "string" }, age: { type: "number" }, }, required: ["name"],}Array of objects:
output: { type: "array", items: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, }, },}Complete example
Section titled “Complete example”import { defineBackend, type InstrumentBackendContext } from "tango-api/backend";
type CreateTaskInput = { stagePath: string; title: string; notes?: string;};
type TaskSummary = { id: string; title: string; status: string;};
export default defineBackend({ kind: "tango.instrument.backend.v2",
onStart: async (ctx) => { await ctx.host.storage.sqlExecute(` CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, stage_path TEXT NOT NULL, title TEXT NOT NULL, notes TEXT DEFAULT '', status TEXT DEFAULT 'todo' ) `); },
actions: { createTask: { input: { type: "object", properties: { stagePath: { type: "string" }, title: { type: "string" }, notes: { type: "string" }, }, required: ["stagePath", "title"], }, handler: async (ctx, input) => { const { stagePath, title, notes } = input as CreateTaskInput; const id = crypto.randomUUID(); await ctx.host.storage.sqlExecute( "INSERT INTO tasks (id, stage_path, title, notes) VALUES (?, ?, ?, ?)", [id, stagePath, title, notes ?? ""] ); ctx.emit({ event: "tasks.changed", payload: { stagePath, taskId: id } }); return { id }; }, },
listStageTasks: { input: { type: "object", properties: { stagePath: { type: "string" } }, required: ["stagePath"], }, output: { type: "array", items: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, status: { type: "string" }, }, }, }, handler: async (ctx, input) => { const { stagePath } = input as { stagePath: string }; return ctx.host.storage.sqlQuery<TaskSummary>( "SELECT id, title, status FROM tasks WHERE stage_path = ?", [stagePath] ); }, }, },});