TypeScript SDK
@loopstack/client is the headless SDK for Loopstack: the same REST + SSE API that Studio and the CLI use, as a typed, framework-free client. It runs in bare Node and in the browser, has no framework dependencies, and every request and response type comes from @loopstack/contracts — the same zod contracts the server validates against.
npm install @loopstack/clientStart a run and stream it — in under 20 lines
Against a local backend with auth disabled, no token or login needed:
import { createClient } from '@loopstack/client';
const client = createClient({ url: 'http://localhost:3000' });
const { data: workspaces } = await client.workspaces.list({ limit: 1 });
const workspace = workspaces[0] ?? (await client.workspaces.create({ appName: 'hello_app', title: 'SDK' }));
const events = client.stream.events(); // subscribe before starting — no event is missed
const run = await client.processor.start({ workflowName: 'hello', workspaceId: workspace.id, args: { name: 'SDK' } });
for await (const event of events) {
if (!('workflowId' in event) || event.workflowId !== run.workflowId) continue;
if (event.type === 'llm.response.text_delta') process.stdout.write(event.delta);
if (event.type === 'workflow.updated' && ['completed', 'failed', 'canceled'].includes(event.status)) break;
}
client.stream.close();
const { result } = await client.workflows.get(run.workflowId);
console.log(result); // → { greeting: 'Hello, SDK! 👋' }Resources
| Resource | What it covers |
|---|---|
client.workflows | Runs: get, status, list (filter/sort/search/pagination), create, update, delete, batchDelete, checkpoints, toolCalls (recorded tool calls of a traced run) |
client.documents | Documents produced by runs: get, list |
client.processor | Execution: start a workflow, run a transition — answering human-in-the-loop prompts is a transition with a payload |
client.workspaces | Workspaces: CRUD, favourites, batch delete |
client.config | What the backend serves: apps (workflows, documents, UI config per @StudioApp), tools, tool, workflowConfig, workflowSource, availableEnvironments |
client.dashboard | stats — recent runs and recent errors |
client.auth | me, workerHealth, hubLogin, refresh, logout |
client.http | The typed fetch layer (get/post/put/patch/delete) — the escape hatch for endpoints without a resource yet |
Live events
client.stream is the backend’s server-sent-events stream, shared by all subscribers of the client. It is lazy — no connection opens until the first subscriber attaches:
// Typed per-event-type handler; returns an unsubscribe function
client.stream.on('workflow.updated', (event) => console.log(event.workflowId, event.status));
// Every event
const off = client.stream.onAny((event) => console.log(event.type));
// Async-iterator form — ideal for following one run
for await (const event of client.stream.events()) {
/* … */
}
client.stream.close();Event types include workflow.updated (place and status changes), document.*, and llm.response.* (token deltas while an LLM generates).
Authentication
const client = createClient({
url: 'https://my-backend.example.com',
token: process.env.LOOPSTACK_TOKEN, // personal access token (lsk_…) or JWT
});- Token — pass a personal access token (created via
POST /api/v1/auth/tokensor Studio) or a JWT; it is sent as a bearer credential on every request and on the event stream. - Cookies — omit
tokenin the browser;credentialsdefaults to'include'. - Local development — a backend with auth disabled needs neither.
fetch can be injected for testing or instrumentation.
Query descriptors
Every read is also available as a { queryKey, queryFn } descriptor under client.queries.*, with keys scoped by the client’s envKey (defaults to the URL) so two environments never share cache entries. Plug them into any TanStack-Query-compatible cache — or use @loopstack/react for ready-made hooks with live invalidation.