Tutorial 4: Backend Actions
Backend actions run in the host Bun process — outside the browser sandbox. Use them for file system access, network requests, heavy computation, or anything that doesn’t belong in the frontend.
Add a backend entry point
Section titled “Add a backend entry point”Update your manifest in package.json:
{ "tango": { "instrument": { "backendEntrypoint": "./dist/backend.js" } }}Define the backend
Section titled “Define the backend”Create src/backend.ts:
import { defineBackend, type InstrumentBackendContext } from "tango-api/backend";
export default defineBackend({ kind: "tango.instrument.backend.v2", actions: { greet: { input: { type: "object", properties: { name: { type: "string" }, }, }, output: { type: "object", properties: { greeting: { type: "string" }, timestamp: { type: "number" }, }, required: ["greeting", "timestamp"], }, handler: async (ctx, input) => { const name = (input as { name?: string })?.name ?? "world"; return { greeting: `Hello, ${name}!`, timestamp: Date.now(), }; }, }, },});Anatomy of an action
Section titled “Anatomy of an action”Each action has three parts:
| Part | Required | Description |
|---|---|---|
input | No | JSON schema describing the expected input |
output | No | JSON schema describing the return value |
handler | Yes | Async function (ctx, input) => output |
The context object
Section titled “The context object”Every handler receives an InstrumentBackendContext as its first argument:
type InstrumentBackendContext = { instrumentId: string; permissions: InstrumentPermission[]; emit: (event: { event: string; payload?: unknown }) => void; host: { storage: StorageAPI; sessions: SessionsAPI; connectors: ConnectorsAPI; stages: StageAPI; events: HostEventsAPI; settings: InstrumentSettingsAPI; };};You have full access to the same APIs as the frontend, plus the ability to emit events back to the UI.
Call from the frontend
Section titled “Call from the frontend”Use useInstrumentAction to create a typed caller:
import { useInstrumentAction, useInstrumentApi, UIRoot, UIPanelHeader, UISection, UICard, UIButton, UIInput } from "tango-api";import { useState } from "react";
type GreetInput = { name: string };type GreetOutput = { greeting: string; timestamp: number };
function SidebarPanel() { const api = useInstrumentApi(); const greet = useInstrumentAction<GreetInput, GreetOutput>("greet"); const [name, setName] = useState(""); const [result, setResult] = useState<GreetOutput | null>(null);
const handleGreet = async () => { const output = await greet({ name }); setResult(output); };
return ( <UIRoot> <UIPanelHeader title="Greeter" /> <UISection> <UICard> <UIInput value={name} placeholder="Your name" onInput={setName} /> <UIButton label="Greet" variant="primary" onClick={handleGreet} /> {result && <p>{result.greeting} (at {new Date(result.timestamp).toLocaleTimeString()})</p>} </UICard> </UISection> </UIRoot> );}How useInstrumentAction works
Section titled “How useInstrumentAction works”const greet = useInstrumentAction<TInput, TOutput>("greet");// Returns: (input?: TInput) => Promise<TOutput>It’s a thin wrapper around api.actions.call("greet", input) that is stable across re-renders (uses useCallback internally).
Lifecycle hooks
Section titled “Lifecycle hooks”The backend supports onStart and onStop:
export default defineBackend({ kind: "tango.instrument.backend.v2", actions: { /* ... */ }, onStart: async (ctx) => { // Runs when the instrument loads console.log(`Backend started for ${ctx.instrumentId}`); }, onStop: async () => { // Runs when the instrument unloads console.log("Backend stopped"); },});Emitting events from the backend
Section titled “Emitting events from the backend”Backend actions can push updates to the frontend:
handler: async (ctx, input) => { // Do some work... ctx.emit({ event: "task.completed", payload: { taskId: "123" } }); return { success: true };},The frontend receives this via useHostEvent("instrument.event", handler).
Input schemas
Section titled “Input schemas”The ActionSchema type supports these JSON schema types:
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; };Schemas are optional but recommended — they serve as documentation and will enable validation in future SDK versions.
Next steps
Section titled “Next steps”Backend actions are great for logic, but you also need persistent data. In the next tutorial you’ll learn about the three storage tiers.