TypeScript SDK
Deploy agents with the CLI, then use the SDK to create sessions, run tasks, and stream responses from your app.
Install
bash
npm install swarmlordThe SDK resolves the API key from SWARMLORD_API_KEY automatically. You can also pass it explicitly:
typescript
import { createClient } from "swarmlord";
const client = createClient({ apiKey: process.env.SWARMLORD_API_KEY });TIP
The SDK does not read the auth file from swarmlord auth login — use the env var or pass apiKey explicitly.
Agents
client.agent(name) references a deployed agent by the name field in its swarmlord.jsonc. All configuration — model, tools, instructions, skills, workspace files — comes from the deployment.
typescript
const bot = client.agent("support-bot");Override settings per handle:
typescript
const reviewer = client.agent({
name: "code-reviewer",
model: "anthropic/claude-sonnet-4.6",
});Resume an existing session by ID:
typescript
const session = client.agent("my-agent").session("existing-session-id");
await session.send("Continue where we left off");Sessions — Interactive Conversations
Sessions are stateful, multi-turn, and persistent. The agent's sandbox is seeded with deployed workspace files automatically.
typescript
const session = await client.agent("my-agent").createSession({
title: "Build Todo App",
});
const result = await session.send("Create a REST API with Hono", {
onText: delta => process.stdout.write(delta),
});
// Continue the conversation
await session.send("Now add authentication");
await session.end();Streaming Callbacks
session.send() accepts callback functions for real-time events:
typescript
const result = await session.send("Analyze this data", {
onConnect: () => console.log("Connected"),
onDisconnect: () => console.log("Reconnecting..."),
onText: delta => process.stdout.write(delta),
onReasoning: delta => {
/* reasoning tokens */
},
onToolStart: part => console.log(`[tool] ${part.tool}`),
onToolComplete: part => console.log(`[done] ${part.tool}`),
onToolError: (part, error) => console.error(`[error] ${part.tool}: ${error}`),
onStepFinish: (tokens, cost) => console.log(`$${cost.toFixed(4)}`),
onAttachment: file => console.log(`File: ${file.filename}`),
onPermission: async req => "once", // "once" | "always" | "reject"
onStatus: status => console.log(`Status: ${status}`),
onSessionIdle: sessionId => console.log(`Idle: ${sessionId}`),
onError: err => console.error(err),
// Memory events (when the agent has `memory: { enabled: true }`)
onMemoryRecalled: e => console.log(`recalled ${e.entryCount} entries`),
onMemoryExtracted: e => console.log(`extracted ${e.opsApplied}/${e.opsProposed} ops`),
onMemoryWrote: e => console.log(`wrote [${e.category}] id=${e.id}`),
onMemoryBlocked: e => console.warn(`blocked: ${e.reasons.join(",")}`),
// Universal firehose — fires once per event, BEFORE specific callbacks.
// Useful for custom logging or handling event types without a specific callback.
onEvent: e => log.debug(e.type, e),
});Every specific callback still fires; onEvent is an escape hatch that sees every AgentEvent variant including any future ones.
Async Iterator
For full control, use session.stream(). Note that stream() does not automatically reply to permission requests — use send() with onPermission for that, or handle permission-asked events and call replyToPermission() manually.
typescript
for await (const event of session.stream("Add tests")) {
switch (event.type) {
case "connect":
console.log("Connected");
break;
case "text-delta":
process.stdout.write(event.delta);
break;
case "tool-start":
console.log(`\n[${event.part.tool}]`);
break;
case "step-finish":
console.log(`\nStep: $${event.cost.toFixed(4)}`);
break;
}
}File Attachments
Send files alongside messages using the parts-based input:
typescript
import { readFileSync } from "node:fs";
const csvBase64 = readFileSync("data.csv").toString("base64");
await session.send({
parts: [
{ type: "text", text: "Analyze this CSV and produce a chart." },
{
type: "file",
url: `data:text/csv;base64,${csvBase64}`,
mime: "text/csv",
filename: "data.csv",
},
],
});Sandbox Access
Run shell commands and read/write files directly without going through the LLM:
typescript
const result = await session.shell("npm test");
console.log(result.stdout);
console.log(`Exit code: ${result.exitCode}`);
const file = await session.getFile("/workspace/output.json");
const image = await session.getFileBuffer("/workspace/chart.png");
await session.putFile("/workspace/config.json", JSON.stringify({ key: "value" }));
await session.putFileBuffer("/workspace/input.pdf", readFileSync("doc.pdf"));Verify Outputs
Check whether expected files exist in the workspace after a send:
typescript
const missing = await session.verifyOutputs(["/workspace/report.md", "/workspace/chart.png"]);
if (missing.length) console.warn("Missing:", missing);Commands
Run a named command defined in the agent's config:
typescript
const output = await session.runCommand("lint", { path: "./src" });Permissions
Reply to a permission request outside of the onPermission callback (useful with stream() or events()):
typescript
await session.replyToPermission(request.id, "once", "Approved by automation");Raw Event Stream
Get the underlying SSE stream for custom consumption. Supports Last-Event-ID replay:
typescript
const stream = await session.events(lastEventId);Session Lifecycle
typescript
await session.summarize(); // Compact context (free up token budget)
await session.revert(messageId); // Undo to a specific message
await session.abort(); // Cancel the current turn
await session.end(); // Clean up the sessionSession Inspection
typescript
const info = await session.getInfo();
const messages = await session.getMessages();
const metrics = await session.getMetrics();
const todos = await session.getTodos();
const artifact = await session.getArtifact("report");
const artifactBuf = await session.getArtifactBuffer("chart");Session Metadata
typescript
await session.update({ title: "New Title", archived: true });Resume or Fork
Resume a session from anywhere using client.session(id) — no agent reference needed:
typescript
// Resume from another process
const session = client.session("existing-session-id");
await session.send("Continue where we left off");
// Fork to try an alternative approach
const fork = await client.agent("my-agent").forkSession("session-to-fork");
await fork.send("Try a different approach");Tasks — Fire-and-Forget
Launch a task, walk away. The agent runs in the cloud and you poll or wait for the result.
typescript
const task = await client
.agent("code-reviewer")
.run("Review all TypeScript files in the project", { title: "Code Review" });
// Poll manually
const status = await task.poll(); // "accepted" | "busy" | "compacting" | "idle" | "error"
// Or block until done (default timeout: 600_000ms / 10 minutes)
const result = await task.result({ timeoutMs: 300_000 });
console.log(result.text);
console.log(`Cost: $${result.cost?.toFixed(4)}`);
// Cancel if needed
await task.cancel();Live Stream
Tasks can also be tailed live via SSE, same as session.stream():
typescript
const task = await client.agent("reviewer").run("Analyze this PR");
for await (const event of task.stream()) {
if (event.type === "tool-complete") console.log(`tool ${event.part.tool} done`);
if (event.type === "session-idle") break;
}Pass { since: <eventId> } to replay from a specific point in the durable event log. See Observability for the full model.
Webhooks
Get notified when a task completes:
typescript
const task = await client.agent("reviewer").run("Analyze this PR", { webhook: "https://my-app.com/hooks/done" });Resume a Task
typescript
const task = client.task(sessionId);
const result = await task.result();SendResult
Every session.send() resolves to a SendResult:
typescript
interface SendResult {
messageId: string;
text: string; // Full text output
parts: Part[]; // All message parts (text, tools, files, etc.)
tokens?: Tokens; // Token usage
cost?: number; // Cost in USD
error?: { name: string; data: Record<string, unknown> };
incomplete?: boolean; // True if SSE stream dropped before completion
outputErrors?: string[]; // Paths from expectedOutputs that were not found
}Listing Resources
typescript
const sessions = await client.listSessions({ limit: 20, archived: false });
const agents = await client.listDeployedAgents();
const schedules = await client.listSchedules();
const triggers = await client.listTriggers();
const { data: models } = await client.listModels();Schedules
Manage deployed scheduled agents:
typescript
const schedule = client.schedule("schedule-id");
await schedule.trigger(); // Manually fire a scheduled run
await schedule.pause(); // Pause the cron
await schedule.resume(); // Resume the cron
await schedule.update({ cron: "0 10 * * 1-5" });
await schedule.delete();Triggers
Manage deployed webhook triggers:
typescript
const trigger = client.trigger("trigger-id");
const info = await trigger.getInfo(); // Get webhook URL, secret, status
await trigger.pause();
await trigger.resume();
await trigger.update({ events: ["app_mention"] });
await trigger.delete();Memory
Cross-session structured recall, scoped to a deployed agent. Requires memory: { enabled: true } in the agent's swarmlord.jsonc. See the Memory concept page for the full picture.
typescript
const agent = client.agent({ name: "support-bot" });
await agent.memory.add({ category: "preference", content: "User prefers concise replies" });
const hits = await agent.memory.search("reply style", { limit: 5 });
const all = await agent.memory.list({ category: "preference", limit: 50 });
await agent.memory.forget(hits[0].id);Categories: fact, preference, instruction, relationship, skill_observation (any string is accepted).
Entries are soft-deleted and kept as audit trail (deletedAt, supersededBy). Injection into the system prompt happens automatically at the start of each session turn loop, wrapped in a <memory-context> fence so it never overrides your agent's instructions.
Configuration
Read and update user-level configuration:
typescript
const config = await client.getConfig();
await client.updateConfig({
model: "anthropic/claude-sonnet-4.6",
compaction: { auto: true },
});Workflows
Workflows are durable multi-step programs that coordinate sessions, ask humans for approval, and wait for timers or external events — surviving deploys and resuming exactly where they paused. See the Workflows guide for node types and semantics.
typescript
import { createClient, defineWorkflow } from "swarmlord";
const client = createClient();
const morningBrief = defineWorkflow({
startNodeId: "research",
sharedSession: { title: "morning brief" },
nodes: {
research: {
id: "research",
type: "task",
prompt: "Read my inbox and list the 3 most important emails.",
saveAs: "emails",
next: "confirm",
},
confirm: {
id: "confirm",
type: "approval",
question: "Send the top 3 emails as an SMS summary now?",
options: ["yes", "no"],
next: { yes: "send", no: "done" },
},
send: {
id: "send",
type: "task",
prompt: "Compose a concise SMS with: {{emails}}",
next: "done",
},
done: { id: "done", type: "end", result: "emails" },
},
});
const handle = await client.startWorkflow({
definition: morningBrief,
title: "Morning brief",
});
const info = await handle.result();
console.log(info.status); // "completed" | "failed" | "cancelled"WorkflowHandle methods:
| Method | Returns | Description |
|---|---|---|
status() | Promise<WorkflowStatus> | Current lifecycle status. |
get() | Promise<WorkflowInfo> | Full workflow state including vars and last step. |
history() | Promise<WorkflowStepInfo[]> | Ordered step history with timings and outcomes. |
result() | Promise<WorkflowInfo> | Polls until the workflow reaches a terminal state. |
stream() | Promise<ReadableStream> | SSE stream of workflow events (node transitions, task progress). |
cancel() | Promise<void> | Cancels the workflow at its next checkpoint. |
Reconnect to an existing workflow with client.workflow(id).
Metrics and Cost
Every session tracks token usage and cost across all services:
typescript
const metrics = await session.getMetrics();
console.log(`Total cost: $${metrics.cost.toFixed(4)}`);
console.log(`LLM: $${metrics.costBreakdown.llm.toFixed(4)}`);
console.log(`Steps: ${metrics.steps}, Tool calls: ${metrics.toolCalls}`);SessionMetrics includes:
| Field | Type | Description |
|---|---|---|
sessionId | string | Session identifier |
messages | number | Total messages in the conversation |
steps | number | LLM inference steps |
toolCalls | number | Total tool invocations |
toolUsage | Record<string, number> | Per-tool call counts |
tokens | Tokens | Aggregate token usage (input, output, reasoning, cache) |
persistBytes | number | Persistent storage size in bytes |
cost | number | Total cost in USD |
costBreakdown | CostBreakdown | Per-service breakdown (LLM, container, DO, R2, Exa, egress, service fee) |
Per-step cost is also available via the onStepFinish callback and the step-finish event.
Shell Result
session.shell() returns a ShellResult:
typescript
interface ShellResult {
stdout: string;
stderr: string;
exitCode: number;
}Task Result
task.result() resolves to a TaskResult:
typescript
interface TaskResult {
sessionId: string;
status: "completed" | "error";
text: string;
messages: WithParts[];
tokens?: Tokens;
cost?: number;
}Configuration Fields
client.getConfig() returns the user-level Config:
| Field | Type | Description |
|---|---|---|
model | string | Default model for new sessions |
smallModel | string | Model used for background tasks (titles, summaries) |
defaultAgent | string | Default agent entry name |
instructions | string[] | Additional system instructions prepended to all sessions |
permission | PermissionConfig | Global permission rules |
tools | ToolsConfig | Global tool enable/disable defaults |
command | object | Named commands — each has template, optional description, agent, model, subtask |
compaction | object | Context compaction settings: auto (boolean), prune (boolean), reserved (number) |
Types
All types are exported from "swarmlord" — including AgentHandle, SessionHandle, TaskHandle, WorkflowHandle, SendResult, TaskResult, StreamCallbacks, WorkflowDefinition, StartWorkflowInput, WorkflowInfo, and others. Custom tools use a separate import path: "swarmlord/tool".