TypeScript SDK
This page covers how to install, configure, and use the official @web-agent/sdk Node / browser package: open a session, run a task, and stream events.
npm install @web-agent/sdk
# or pnpm add @web-agent/sdk / yarn add @web-agent/sdk / bun add @web-agent/sdkWorks in Node 20+ and modern browsers.
Don't ship a server-grade
wa_key to the browser. Keys grant project-wide access; ship them only to server-side code or to environments where you trust the runtime.
One entry point: Client
User-facing API products live on the same Client:
import { Client } from "@web-agent/sdk";| Resource | Product | Use case |
|---|---|---|
client.sessions / messages / events | DoAnything (open-ended) | Free-form input; the agent picks the path. |
client.deepResearch | DeepResearch (research → report) | Standalone API. |
client.webSearch | WebSearch (query → results) | Synchronous by default (wait: true). |
client.track | Track (monitor → snapshot) | Long-lived monitors with webhook delivery. |
DoAnything — open-ended tasks
import { Client } from "@web-agent/sdk";
const client = new Client({
apiKey: process.env.WEBAGENT_API_KEY!,
projectId: process.env.WEBAGENT_PROJECT_ID!,
});
const session = await client.sessions.create({
instructions: "Search Hacker News for the top 5 stories today.",
});
const task = session.tasks[0]!; // session-create implicitly queues the first task
for await (const event of client.events.stream(session.id, task.id)) {
console.log(event.type, event.data);
if (event.type === "task.completed") break;
}Wire fields stay snake_case to match the API exactly; method names are camelCase.
Resume + heartbeats
for await (const event of client.events.stream(session.id, task.id, {
lastEventId: "142",
includeHeartbeats: false,
})) {
if (event.type === "task.completed") break;
}Backed by fetch with manual SSE parsing — works in Node 20+, Bun, Cloudflare Workers, and modern browsers.
Follow-up task vs. inflight message
// 1. Push into the current task's chat queue
await client.messages.send(session.id, task.id, {
content: "Also include the comment count for each.",
});
// 2. Start a NEW task in the SAME session
const followup = await client.sessions.createTask(session.id, {
instructions: "Click into the first post and summarise it.",
});Answer an input request
await client.messages.intervene(session.id, task.id, {
kind: "answer_input_request",
input_request_id: "ir_01HXX",
response: { solved: true },
});The kind discriminator lets the same endpoint handle take-control / release-control too — see Take Control.
Cancel / stop / list
await client.sessions.cancelTask(session.id, task.id, { reason: "user_cancelled" });
await client.sessions.stop(session.id, { force: false });
const list = await client.sessions.list({ status: "running", limit: 20 });
list.items.forEach((s) => console.log(s.id, s.status));DeepResearch — research → report
DR is a Standalone API (pidless: /v1/deep_research); the project tenant resolves from the Bearer token.
const task = await client.deepResearch.run({
topic: "Open-source vector DB landscape 2026",
depth: "deep", // light / standard / deep
requireOutlineApproval: true, // outline HITL gate (default on)
});
// Subscribe to events (DR uses the DoAnything SSE channel) + respond to the gate
for await (const event of client.events.stream(
task.session_id as string, task.task_id as string,
)) {
if (event.type === "task.input_request") {
await client.deepResearch.intervene(task.task_id as string, {
requestId: (event.data as { request_id: string }).request_id,
response: "approve", // or { action: "approve_with_edits", edits: [...] }
});
}
if (event.type === "task.completed") break;
}
// Pull the three-piece artifact set (final.md / citations.json / confidence.json)
const artifacts = await client.deepResearch.listArtifacts(task.task_id as string);
const final = await client.deepResearch.getArtifact(
task.task_id as string, artifacts[0]!.id as string,
);WebSearch — query → results
WS is project-scoped. run() defaults to wait: true: the server blocks for ≤30s and returns the done envelope; on timeout it returns 202 — call get(taskId) to poll.
// Synchronous (default)
const result = await client.webSearch.run({
queries: ["best TypeScript ORM 2026"],
engines: ["tavily"],
summarize: true,
});
// Async
const pending = await client.webSearch.runAsync({
queries: ["best TypeScript ORM 2026"],
});
const detail = await client.webSearch.get(pending.task_id as string);
// Refine (re-run within the same task)
await client.webSearch.refine(pending.task_id as string, {
text: "add site:reddit.com and re-run",
});Track — long-lived monitors
Track is project-scoped. A monitor is a long-lived background job: cron / interval / event schedule + an extraction goal + a notify channel (webhook). Each tick produces a snapshot; whenever the trigger DSL judges the diff worth notifying, the channel fires.
const mon = await client.track.create({
intent:
"Notify me when the iPhone 17 Pro listing on apple.com goes below $999",
schedule: { kind: "interval", every_seconds: 3600 },
notifyChannel: { kind: "callback_url", url: "https://hooks.example.com/track" },
});
// Lifecycle controls — pause / resume / refine via patch:
await client.track.pause(mon.id as string, { reason: "manual review" });
await client.track.resume(mon.id as string);
await client.track.refine(mon.id as string, {
triggerDsl: { op: "lt", field: "price", value: 999 },
});
// Manually fire a tick (bypasses schedule):
const outcome = await client.track.runNow(mon.id as string);
// Snapshot history (newest first):
const snaps = await client.track.listSnapshots(mon.id as string);
const snap = await client.track.getSnapshot(
mon.id as string,
snaps.items[0]!.id as string,
);
// Webhook outbox + retry:
const deliveries = await client.track.listDeliveries(mon.id as string, {
includePayload: true,
});
await client.track.retryDelivery(
mon.id as string,
deliveries.items[0]!.id as number,
);
// Cancel terminates the monitor (terminal state):
await client.track.cancel(mon.id as string); // equivalent: client.track.delete(...)Alignment HITL (optional)
If the supervisor needs you to disambiguate intent, the monitor moves to pending_clarification and emits an alignment.input_request event. Answer with intervene():
await client.track.intervene(mon.id as string, {
requestId: "req_align_1",
response: "SKU A",
});You can also push free-text guidance into the alignment queue via client.track.message(monId, { content: "…" }).
Errors
import {
ApiError,
InsufficientCreditsError,
RateLimitedError,
UnauthorizedError,
} from "@web-agent/sdk";
try {
await client.sessions.create({ instructions: "…" });
} catch (err) {
if (err instanceof InsufficientCreditsError) {
console.log("top up:", err.detail, err.extra);
} else if (err instanceof ApiError) {
console.log(err.code, err.statusCode, err.detail);
} else {
throw err;
}
}Every error class subclasses ApiError and exposes code / statusCode / detail / extra matching the API error envelope.
Types
DoAnything request / response types are top-level exports:
import type {
Session, Task, Event, EventType,
CreateSessionRequest, CreateTaskRequest, InterveneRequest,
TaskStatus, SessionStatus, TerminalReason,
} from "@web-agent/sdk";DR / DS / WS responses come back as Record<string, unknown> (the OpenAPI envelope verbatim) — index by key (task.task_id / task.status). Each resource also exports its own option types (DRRunOptions / DSRunOptions / WSRunOptions).
Next steps
- Python SDK — same surface in Python.
- Errors & retries — recommended retry policy, idempotency keys.
- Sessions & Tasks — lifecycle, profiles, workspaces.