ql-agent-sdk
v1.4.0
Published
TypeScript SDK for the Quanton Labs agent platform API (v1).
Readme
ql-agent-sdk
TypeScript SDK for the Quanton Labs agent platform API (v1). Integrate your application with 8 specialized AI agents, capabilities, workflows, and background tasks.
- Zero runtime dependencies — built on native
fetchand Web Streams - Full TypeScript — all request/response types exported
- ESM + CJS — works in Node, Bun, Deno, Next.js, and edge runtimes
Base URL defaults to
https://app.quantonlabs.com. Override withbaseUrloption or theQL_BASE_URLenvironment variable.
Install
npm install ql-agent-sdk
# or
pnpm add ql-agent-sdk
# or
yarn add ql-agent-sdkRequires Node.js 18+ (native fetch).
Quick Start
import { QLClient } from "ql-agent-sdk";
const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
// Discover available agents
const { agents } = await ql.agents.list();
console.log(agents.map((a) => a.id));
// → ["governing", "finance", "marketing", "inventory", "customer", "developer", "operations", "research"]
// Chat with an agent (non-streaming)
const reply = await ql.agents.chat({
agentId: "governing",
messages: [{ role: "user", content: "Summarize this quarter's revenue." }],
});
console.log(reply.content);Configuration
const ql = new QLClient({
apiKey, // required — your Bearer API key (ql_live_...)
baseUrl, // optional — defaults to QL_BASE_URL or https://app.quantonlabs.com
fetch: customFetch, // optional — override for tests/proxies/custom runtimes
});The apiKey is held in a private field. It is never returned by the client, included in error messages, or logged.
Authentication
All API calls use Bearer token authentication with your API key:
Authorization: Bearer ql_live_your_key_hereCreate keys in your Quanton Labs dashboard → API Keys page. Each key is scoped to a single workspace.
Scope Requirements
| Scope | Grants access to |
|-------|-----------------|
| workspace | ql.workspace.get(), ql.workspace.getModels(), ql.workspace.update(), ql.workspace.updateModels(), ql.workspace.listCatalogModels(), ql.workspace.verifyOpenRouterKey(), ql.workspace.listKeys() |
| agents | ql.agents.list(), ql.agents.get() |
| capabilities | ql.agents.capabilities(), ql.agents.runCapability() |
| chat | ql.agents.chat(), ql.agents.chatStream() |
| messages | ql.agents.history(), ql.agents.listMessages() |
| workflows | ql.workflows.run(), ql.workflows.getRun() |
| tasks | ql.tasks.create(), ql.tasks.get() |
| * | All of the above (default for new keys) |
API Reference
Response Envelope
All non-streaming endpoints return a consistent envelope:
Success:
{ "ok": true, "data": { ... } }Error:
{ "ok": false, "error": { "code": "not_found", "message": "Agent not found: foo" } }The SDK unwraps data for you and throws a typed QLError on errors. You never see the envelope directly.
Workspace
ql.workspace.get() → Promise<Workspace>
Scope: workspace
Returns the workspace bound to your API key.
Response (200):
{
"_id": "6adf926f6f87c9ab86f32c0e",
"name": "Robin",
"plan": "standard",
"createdAt": "2026-10-26T17:48:31.038Z",
"updatedAt": "2026-10-27T11:56:44.074Z"
}TypeScript:
const ws = await ql.workspace.get();
ws._id; // string
ws.name; // string
ws.plan; // "standard" | "premium" | "enterprise" | undefined
ws.createdAt; // string (ISO 8601)ql.workspace.getModels() → Promise<WorkspaceModelsResult>
Scope: workspace
Returns the per-role model configuration (director panel): defaults for
reasoning, instruct, response, vision, image, video plus ordered
fallback lists. The OpenRouter key is always masked (keyMasked).
ql.workspace.update(input) / ql.workspace.updateModels(input) → Promise<Workspace>
Scope: workspace
Partially updates the workspace. update() accepts { name?, config?: { openrouter? } };
updateModels() is a shortcut that only touches config.openrouter. Model fields may be
set to "" to clear a default; each fallbacks.<role> list holds at most 3 model ids.
await ql.workspace.update({ name: "Robinson Mfg. Co., Inc." });
await ql.workspace.updateModels({
reasoning: "deepseek/deepseek-v4-flash-0731",
vision: "anthropic/claude-sonnet-5.5",
fallbacks: { reasoning: ["openai/gpt-oss-20b"] },
});ql.workspace.listCatalogModels() → Promise<CatalogResult>
Scope: workspace
OpenRouter model catalog ({ id, name, contextLength, promptPrice, completionPrice }[]),
cached server-side for 10 minutes. May return stale: true when upstream is down.
ql.workspace.verifyOpenRouterKey(key, model?, baseUrl?) → Promise<VerifyKeyResult>
Scope: workspace
Checks an OpenRouter key with a 1-token completion (default model
openai/gpt-oss-20b). Returns { valid: true } or { valid: false, error }.
ql.workspace.listKeys() → Promise<KeysResult>
Scope: workspace
Lists the workspace's v1 API keys with rotation metadata. Every key is auto-deleted
lifetimeDays (90) days after creation; rotatesAt/daysLeft show time remaining.
Mint replacements from the Quanton Labs site when one expires.
Agents
ql.agents.list() → Promise<AgentListResult>
Scope: agents
Lists all 8 available agents with their capability counts.
Response (200):
{
"agents": [
{
"id": "governing",
"name": "Governing Agent",
"domain": "orchestration",
"description": "Coordinates all other agents...",
"icon": "brain",
"tags": ["coordination", "reasoning"],
"capabilityCount": 20
}
]
}ql.agents.get(agentId) → Promise<AgentDetail>
Scope: agents
Returns an agent's details and full capability list.
Parameters:
agentId: string— agent slug (e.g."finance","governing") or ObjectId
Response (200):
{
"agent": {
"id": "finance",
"name": "Finance Agent",
"domain": "finance",
"description": "Handles financial analysis...",
"icon": "dollar-sign",
"tags": ["accounting", "budgeting"]
},
"capabilities": [
{
"id": "invoice",
"name": "Invoice Generator",
"description": "Generate formatted invoices from data",
"category": "documents",
"params": [
{ "key": "data", "label": "Data", "kind": "textarea", "required": true }
],
"templates": [
{ "label": "Monthly Report", "values": { "data": "Revenue | 12000\nCosts | 4000" } }
]
}
]
}Errors:
404 not_found— agent slug doesn't exist
ql.agents.capabilities(agentId) → Promise<CapabilitiesResult>
Scope: capabilities
Returns just the capability list for an agent (lighter than get).
Response (200):
{
"agentId": "finance",
"capabilities": [ ... ]
}ql.agents.runCapability(agentId, input) → Promise<RunCapabilityResult>
Scope: capabilities
Executes a specific capability (feature) on an agent.
Parameters:
agentId: string— agent sluginput.capId: string— capability ID (from the capabilities list)input.params?: Record<string, unknown>— capability-specific parameters
Response (200):
{
"result": {
"type": "html",
"html": "<div class=\"invoice\">...</div>"
}
}The result is a discriminated union — check result.type:
| type | Fields | When |
|--------|--------|------|
| "text" | content: string | Plain text output |
| "json" | data: unknown | Structured JSON |
| "html" | html: string | HTML content (renderable) |
| "file" | url, filename, mime | Generated file (image, PDF, etc.) |
Example:
const { result } = await ql.agents.runCapability("finance", {
capId: "invoice",
params: { data: "Revenue | 12000\nCosts | 4000" },
});
if (result.type === "html") {
document.body.innerHTML = result.html;
} else if (result.type === "json") {
console.log(result.data);
} else if (result.type === "text") {
console.log(result.content);
} else if (result.type === "file") {
window.open(result.url); // download
}Chat
ql.agents.chat(input) → Promise<ChatResult>
Scope: chat
Non-streaming chat with an agent. Waits for the full response.
Parameters:
agentId: string— agent slugmessages: ChatMessage[]— conversation history (newest last)userId?: string— optional internal user ID (defaults to workspace)response_format?: object— optional OpenRouter format hint
Response (200):
{
"content": "Based on your data, Q3 revenue was $120K...",
"toolCalls": 2,
"iterations": 3,
"duration": "4.21s"
}| Field | Type | Description |
|-------|------|-------------|
| content | string | The agent's full text response |
| toolCalls | number | How many tool calls were made during reasoning |
| iterations | number | Tool loop iterations |
| duration | string | Human-readable execution time |
Example:
const reply = await ql.agents.chat({
agentId: "finance",
messages: [
{ role: "user", content: "What was our profit margin last month?" },
],
});
console.log(reply.content);ql.agents.chatStream(input, opts?) → AsyncIterable<ChatStreamEvent>
Scope: chat
Streaming chat — yields incremental events as the agent responds.
Parameters:
- Same as
chat()above opts.signal?: AbortSignal— abort the stream at any time
Yielded events:
type ChatStreamEvent =
| { type: "delta"; content?: string; reasoning?: string } // incremental piece
| { type: "done"; content: string; reasoning?: string }; // final accumulated text| Event | When | Fields |
|-------|------|--------|
| delta | For each token/chunk from the model | content (text delta), reasoning (thinking delta, if model supports it) |
| done | Once at the end | content (full accumulated text), reasoning (full reasoning, if any) |
Example:
import { QLClient } from "ql-agent-sdk";
const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
const controller = new AbortController();
// Optional: auto-abort after 30s
const timer = setTimeout(() => controller.abort(), 30_000);
let full = "";
try {
for await (const ev of ql.agents.chatStream(
{ agentId: "governing", messages: [{ role: "user", content: "Explain quantum computing" }] },
{ signal: controller.signal },
)) {
if (ev.type === "delta") {
process.stdout.write(ev.content ?? ""); // print incrementally
full += ev.content ?? "";
} else if (ev.type === "done") {
full = ev.content; // final text (replaces accumulated)
}
}
} catch (err) {
if ((err as any)?.code === "aborted") console.log("\n[stopped by user]");
} finally {
clearTimeout(timer);
}SSE wire format (if you ever need to parse it manually):
data: {"id":"gen-...","choices":[{"delta":{"content":"Hel"}}]}
data: {"id":"gen-...","choices":[{"delta":{"content":"lo"}}]}
data: [DONE]
Events are separated by blank lines. data: [DONE] signals the end.
History / Messages
ql.agents.history(agentId, opts?) → Promise<HistoryPage>
Scope: messages
Returns paginated conversation history for an agent (newest first).
Parameters:
agentId: string— agent slugopts.limit?: number— page size, 1–100 (default 20)opts.before?: string— cursor from previous page'snextBeforeopts.userId?: string— optional filter by user
Response (200):
{
"agentId": "finance",
"limit": 20,
"nextBefore": "6ae0d96274f1cf895ef06e0c",
"items": [
{
"id": "6ae0d963...",
"role": "user",
"content": "What was our profit margin?",
"toolCalls": [],
"status": "completed",
"timestamp": "2026-10-27T14:32:00.000Z"
},
{
"id": "6ae0d962...",
"role": "assistant",
"content": "Based on your records...",
"reasoning": "Let me check the financial data...",
"toolCalls": [{ "name": "db_query", ... }],
"status": "completed",
"timestamp": "2026-10-27T14:32:05.000Z"
}
]
}| Field | Description |
|-------|-------------|
| nextBefore | Cursor for next page. null when no more pages. |
| items[].role | "user" or "assistant" |
| items[].content | Message text (may be null for tool-only messages) |
| items[].reasoning | Model's thinking (if supported by the model) |
| items[].toolCalls | Array of tool invocations made during this message |
| items[].status | "completed", "processing", or "failed" |
Walking all pages:
const allMessages = [];
let before: string | undefined;
do {
const page = await ql.agents.history("finance", { limit: 50, before });
allMessages.push(...page.items);
before = page.nextBefore ?? undefined; // null when exhausted
} while (before);
console.log(`${allMessages.length} total messages`);ql.agents.listMessages(params) → Promise<AgentMessagesPage>
Scope: messages
Returns the chronological message feed for a conversation — chat turns plus workflow artifacts (tool results, step progress, approval requests). Use this to poll live progress of a background run: call it without after for the latest N, then page forward with after.
Parameters:
params.agentId: string— agent slug or ObjectId (required)params.after?: string— cursor = id of the last item you received (forward pagination). Omit for the latest N.params.limit?: number— page size, 1–50 (default 20)params.userId?: string— optional filter by user (defaults to the workspace id)
Response (200):
{
"items": [
{
"id": "6ae0d963...",
"role": "user",
"kind": "chat",
"content": "Import this CSV into users",
"status": "completed",
"timestamp": "2026-11-05T10:00:00.000Z"
},
{
"id": "6ae0d964...",
"role": "assistant",
"kind": "workflow",
"content": null,
"taskId": "6ae0d965...",
"toolCalls": [{ "name": "fetch_schema", "status": "completed", "result": { "collections": ["users"] } }],
"status": "completed",
"timestamp": "2026-11-05T10:00:02.000Z"
},
{
"id": "6ae0d966...",
"role": "assistant",
"kind": "approval_request",
"content": "Delete 3 rows from `users`? ...",
"outputFormat": "json",
"status": "completed",
"timestamp": "2026-11-05T10:00:05.000Z"
}
],
"hasMore": true,
"nextAfter": "6ae0d966..."
}| Field | Description |
|-------|-------------|
| items[].kind | "chat" (default), "tool_result", "workflow", or "approval_request" |
| items[].content | Message text (may be null for artifact-only entries) |
| items[].outputFormat | Declared format of content: "text", "json", or "html" |
| items[].toolCalls | Tool invocations; each may carry a truncated result once complete |
| items[].taskId | Task id for entries produced by a background workflow run |
| hasMore / nextAfter | Forward pagination: pass nextAfter as the next after |
Polling a live run (e.g. every 4s):
let after: string | undefined;
const timer = setInterval(async () => {
const page = await ql.agents.listMessages({ agentId: "operations", limit: 20, after });
for (const item of page.items) renderFeedItem(item); // tool chips, step progress, approvals…
after = page.nextAfter;
}, 4000);Workflows
ql.workflows.run(workflowId, input?) → Promise<WorkflowRunStart>
Scope: workflows
Triggers a workflow execution. Returns immediately with a run ID (HTTP 202).
Parameters:
workflowId: string— the workflow's ObjectIdinput?: { input?: Record<string, unknown> }— optional input payload for the workflow
Response (202):
{
"runId": "6ae101c8...",
"status": "running"
}Errors:
400 validation_error— invalid workflow ID format404 not_found— workflow doesn't exist in this workspace
ql.workflows.getRun(runId) → Promise<WorkflowRun>
Scope: workflows
Polls a workflow run's status.
Response (200):
{
"runId": "6ae101c8...",
"status": "completed",
"output": { "result": "..." },
"error": null,
"durationMs": 4523,
"startedAt": "2026-10-27T14:32:00.000Z",
"finishedAt": "2026-10-27T14:32:04.523Z"
}| status value | Meaning |
|----------------|---------|
| "queued" | Waiting to start |
| "running" | Currently executing |
| "completed" | Finished successfully — check output |
| "failed" | Execution error — check error |
| "cancelled" | Was cancelled |
Polling example:
const start = await ql.workflows.run("6adf929d6f87c9ab86f32c19", { input: { a: 1 } });
// Poll until done
let run;
do {
await new Promise((r) => setTimeout(r, 2000));
run = await ql.workflows.getRun(start.runId);
} while (run.status === "running" || run.status === "queued");
if (run.status === "completed") console.log(run.output);
else if (run.status === "failed") console.error(run.error);Tasks (Background)
ql.tasks.create(input) → Promise<TaskCreated>
Scope: tasks
Creates a background task. The task starts executing immediately on the server — you don't need to call another endpoint.
Parameters:
agentId: string— which agent handles this tasktitle: string— human-readable namedescription?: string— optional longer descriptioninput?: unknown— payload for the agent (string, or{ message }, or{ prompt })priority?: "low" | "medium" | "high" | "urgent"— scheduling priority
Response (201):
{
"taskId": "6ae101b7...",
"status": "queued"
}Errors:
400 validation_error— missingagentIdortitle404 not_found— unknown agent slug
ql.tasks.get(taskId) → Promise<Task>
Scope: tasks
Single poll of a task's current state.
Response (200):
{
"taskId": "6ae101b7...",
"status": "completed",
"title": "Monthly Report",
"progress": {
"current": 100,
"total": 100,
"message": "Completed",
"updatedAt": "2026-10-27T19:53:23.117Z"
},
"result": "Here is your monthly report...",
"output": "Here is your monthly report...",
"error": null,
"createdAt": "2026-10-27T19:53:16.756Z",
"completedAt": "2026-10-27T19:53:23.117Z"
}| status value | Meaning | Check field |
|----------------|---------|-------------|
| "queued" | Waiting to start | — |
| "running" | In progress | progress.message for current step |
| "retrying" | Failed, will retry automatically | error for what went wrong |
| "completed" | Done successfully | result / output for the answer |
| "failed" | Permanently failed (max retries) | error for the reason |
| "cancelled" | Was cancelled | — |
ql.tasks.handle(taskId) → TaskHandle
Returns a handle with poll() and wait() methods.
handle.poll(signal?) — single fetch, returns current Task snapshot.
handle.wait(opts?) — polls until terminal status with backoff.
| Option | Default | Description |
|--------|---------|-------------|
| intervalMs | 2000 | Initial delay between polls (ms) |
| maxIntervalMs | 15000 | Backoff cap (grows 1.5× per poll) |
| timeoutMs | undefined (wait forever) | Throw after this many ms total |
| signal | undefined | AbortSignal for cancellation |
| onTick | undefined | Callback after each poll with latest snapshot |
Terminal states returned by wait():
task.status === "completed"→ checktask.resulttask.status === "failed"→ checktask.errortask.status === "cancelled"→ task was cancelled server-side
Non-terminal exits (throws QLError):
err.code === "timeout"→ exceededtimeoutMserr.code === "aborted"→signalwas aborted
Example:
const created = await ql.tasks.create({
agentId: "finance",
title: "Quarterly Report",
input: { message: "Generate a Q3 financial summary from our database." },
priority: "high",
});
const handle = ql.tasks.handle(created.taskId);
try {
const done = await handle.wait({
intervalMs: 2000,
maxIntervalMs: 10000,
timeoutMs: 5 * 60_000, // 5 minute timeout
onTick: (task) => {
if (task.status === "running" && task.progress?.message) {
process.stdout.write(`\r${task.progress.message}`);
}
},
});
if (done.status === "completed") {
console.log("\n✓ Done:", done.result);
} else if (done.status === "failed") {
console.error("✗ Failed:", done.error);
}
} catch (err) {
if ((err as any).code === "timeout") console.error("Timed out after 5 min");
if ((err as any).code === "aborted") console.log("Cancelled by user");
}Note on execution: Tasks execute in the server's process. If the server restarts mid-execution, the task will be recovered and retried on next startup (up to
maxRetriestimes). For mission-critical workloads with strict durability requirements, consider a persistent queue backend.
Videos (Tiered Generation)
Async video generation: submit a job, then poll until it completes. Reference images may be base64 data URLs or https URLs; reference (style) videos must be https URLs — upload local files with ql.files.upload first.
ql.videos.listModels() → Promise<VideoModelsResult>
Lists the tiered video models plus runtime engine availability: { models: VideoModelInfo[], engine: { available, detail } }. Each model reports its tier, supported aspect ratios/resolutions, duration range, reference-image/video limits, voiceover support, and pricing.
Example:
const { models, engine } = await ql.videos.listModels();
if (!engine.available) console.warn("Video engine unavailable:", engine.detail);
const recommended = models.find((m) => m.recommended && m.available);ql.videos.generate(agentId, input) → Promise<VideoTaskCreated>
Starts a video job (HTTP 202) and returns { taskId, status: "queued" }.
| Field | Description |
|-------|-------------|
| prompt | The creative brief / script for the video |
| model? | Explicit registry model id (wins over tier) |
| tier? | Tier to resolve a model from when model is omitted |
| images_list? | Product/reference images — base64 data URLs or https URLs (order defines IMG_0..N) |
| video_files? | Reference (style) videos — https URLs only |
| aspect_ratio? | e.g. "16:9", "9:16", "1:1" (default "16:9") |
| duration? | Seconds, within the model's durationRange |
| resolution? | e.g. "720p" / "1080p" (default "720p") |
| generate_audio? | Bake a voiceover into the render (requires narration) |
| narration? | Voiceover text (max 3000 chars) |
Example:
const created = await ql.videos.generate("marketing", {
prompt: "A 10-second product reel for the new sneaker drop, studio lighting.",
model: "motion/reel",
images_list: ["data:image/png;base64,..."], // or https URLs
video_files: ["https://cdn.example.com/style-ref.mp4"],
aspect_ratio: "9:16",
duration: 10,
resolution: "1080p",
generate_audio: true,
narration: "Introducing the all-new AirFlex.",
});ql.videos.getTask(taskId, signal?) → Promise<VideoTask>
Fetches the current job snapshot: { taskId, status, progress?, outputUrl?, mime?, ext?, durationSec?, error?, model?, tier?, latencyMs?, createdAt, completedAt }.
ql.videos.cancelTask(taskId) → Promise<VideoTask>
Cancels a queued/rendering job and returns the updated snapshot. Throws a QLError with code "conflict" (HTTP 409) if the job is already terminal.
ql.videos.handle(taskId) → VideoTaskHandle
Same poll() / wait(opts?) pattern as TaskHandle, polling /api/v1/video-tasks/:taskId. Terminal statuses: completed (check outputUrl), failed (check error), cancelled. Wait options and non-terminal exits (timeout / aborted) are identical to tasks.
Example:
const done = await ql.videos.handle(created.taskId).wait({
intervalMs: 2000,
timeoutMs: 10 * 60_000,
onTick: (job) => {
if (job.progress?.message) process.stdout.write(`\r${job.progress.message}`);
},
});
if (done.status === "completed" && done.outputUrl) {
console.log("Video ready:", `${ql.baseUrl}${done.outputUrl}`);
}Audio (Text-to-Speech)
ql.audio.voices() → Promise<VoicesResult>
Lists available TTS voices: { voices: { id, name }[], source: "live" | "none", note? }. source === "none" means no provider key is configured server-side.
ql.audio.textToSpeech(agentId, input) → Promise<TtsResult>
Synchronous synthesis (no polling). Returns { url, voiceId, modelId, bytes, latencyMs } where url is a served media path (e.g. /generated/tts-….mp3).
| Field | Description |
|-------|-------------|
| text | Text to synthesize (max 3000 chars) |
| voiceId? | Voice id from ql.audio.voices() (defaults to the platform voice) |
| stability? | Voice stability 0..1 (default 0.5) |
| similarityBoost? | Similarity boost 0..1 (default 0.75) |
Example:
const { voices } = await ql.audio.voices();
const result = await ql.audio.textToSpeech("marketing", {
text: "Say hello to the all-new AirFlex.",
voiceId: voices[0]?.id,
});
console.log(`${ql.baseUrl}${result.url}`);Files (Media Upload)
ql.files.upload(file, opts?) → Promise<UploadedFile>
Uploads a media file as multipart form data (field "file") and returns { url, filename, size, contentType }. The returned url is a stable served path (e.g. /uploads/<workspace>/<file>) that can be used directly as a media input URL — e.g. in videos.generate({ video_files: [res.url] }).
| Option | Description |
|--------|-------------|
| filename? | Override the stored display name (extension is kept from the file) |
| onProgress? | Coarse progress callback — fetch does not expose byte progress, so it reports 0 at start and 100 on completion only |
Example:
const blob = await file.arrayBuffer(); // or any Blob source
const uploaded = await ql.files.upload(new Blob([blob], { type: "video/mp4" }), {
filename: "style-ref.mp4",
});Error Handling
All API failures throw a typed QLError:
import { QLClient, QLError } from "ql-agent-sdk";
try {
await ql.agents.chat({ agentId: "finance", messages: [...] });
} catch (err) {
if (err instanceof QLError) {
console.error(err.status); // HTTP status code (401, 403, 404, etc.)
console.error(err.code); // stable string code (see table below)
console.error(err.message); // human-readable (never includes API key)
// Convenience booleans:
err.isAuthError; // true for 401/403
err.isAborted; // true for abort
err.isTimeout; // true for wait() timeout
err.isNetworkError; // true for connection failures
}
}Error Code Reference
| HTTP Status | code | When it happens |
|-------------|--------|-----------------|
| 401 | unauthorized | Missing, invalid, or revoked API key |
| 403 | forbidden | Key lacks the required scope for this endpoint |
| 404 | not_found | Resource doesn't exist in your workspace (agent, workflow, task) |
| 409 | conflict | Conflict (e.g. duplicate resource) |
| 400 | validation_error | Malformed request body, missing required fields, invalid params |
| 500 | internal_error | Server-side error (AI provider failure, DB issue, etc.) |
| — | network | No HTTP response received (connection refused, DNS failure, timeout) |
| — | aborted | Caller aborted via AbortSignal |
| — | timeout | wait() exceeded its timeoutMs budget |
Common Error Scenarios
// 1. Expired/revoked key
try {
await ql.workspace.get();
} catch (e) {
if (e instanceof QLError && e.code === "unauthorized") {
console.log("API key is invalid or revoked. Generate a new one in your dashboard.");
}
}
// 2. Key doesn't have chat scope
try {
await ql.agents.chat({ agentId: "finance", messages: [...] });
} catch (e) {
if (e instanceof QLError && e.code === "forbidden") {
console.log("This key needs the 'chat' scope. Create a new key with broader scopes.");
}
}
// 3. AI provider error (transient)
try {
await ql.agents.chat({ agentId: "governing", messages: [...] });
} catch (e) {
if (e instanceof QLError && e.status === 500) {
console.log("AI service temporarily unavailable. Retry later.");
}
}TypeScript
Full type declarations are included. All public types are exported:
import type {
QLClient,
QLClientOptions,
QLError,
// Request types
ChatInput,
RunCapabilityInput,
TaskCreateInput,
WaitOptions,
HistoryParams,
WorkflowRunInput,
// Response types
Workspace,
AgentSummary,
AgentDetail,
CapabilityInfo,
CapabilityResult,
ChatResult,
ChatStreamEvent,
HistoryPage,
MessageItem,
WorkflowRun,
Task,
TaskCreated,
// Constants
API_ERROR_CODES,
} from "ql-agent-sdk";Using in Next.js App Router
The SDK is safe to use server-side in Route Handlers or Server Components:
// app/api/agent/route.ts
import { QLClient, QLError } from "ql-agent-sdk";
export const runtime = "nodejs"; // uses native fetch + Web Streams
const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
export async function POST(req: Request) {
const { message } = await req.json();
try {
const reply = await ql.agents.chat({
agentId: "governing",
messages: [{ role: "user", content: message }],
});
return Response.json({ text: reply.content });
} catch (err) {
if (err instanceof QLError) {
return Response.json({ error: err.message }, { status: err.status ?? 500 });
}
throw err;
}
}For streaming in a Route Handler:
export async function POST(req: Request) {
const { message } = await req.json();
const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
const stream = new ReadableStream({
async start(controller) {
try {
for await (const ev of ql.agents.chatStream({
agentId: "governing",
messages: [{ role: "user", content: message }],
})) {
if (ev.type === "delta" && ev.content) {
controller.enqueue(new TextEncoder().encode(ev.content));
}
}
} finally {
controller.close();
}
},
});
return new Response(stream, { headers: { "Content-Type": "text/plain" } });
}Agent Reference
The platform includes 8 specialized agents. Each has 20 capabilities in its domain:
| ID | Domain | Examples |
|----|--------|----------|
| governing | Orchestration | Coordinates other agents, complex multi-step reasoning |
| finance | Finance | Invoices, P&L, budgeting, cash flow, tax estimates |
| marketing | Marketing | Social media content, ad copy, SEO, campaign planning |
| inventory | Supply Chain | Stock levels, reorder points, supplier management |
| customer | Customer Success | Support drafts, onboarding sequences, churn analysis |
| developer | Engineering | Code generation, API docs, bug triage, test writing |
| operations | Operations | SOPs, process mapping, KPI dashboards, compliance |
| research | Research | Market analysis, competitor intel, literature reviews |
Discover available capabilities at runtime:
const detail = await ql.agents.get("finance");
console.log(detail.capabilities.map((c) => c.id));Environment Variables
| Variable | Purpose |
|----------|---------|
| QL_API_KEY | Your API key (convention — the SDK reads it from the apiKey option, not env directly) |
| QL_BASE_URL | Override the base URL (useful for staging/self-hosted) |
Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| QLError: unauthorized | Key is invalid, revoked, or expired | Generate a new key in your dashboard |
| QLError: forbidden | Key scope doesn't cover the endpoint | Create a key with the required scope or * |
| QLError: not_found (agent) | Typo in agent slug | Use ql.agents.list() to see valid IDs |
| QLError: network | Can't reach the API | Check your network / base URL / firewall |
| Task stuck in "running" | Server restarted mid-execution | Wait for stale recovery (≤5 min) or poll again later |
| Stream produces no output | Model returned empty response | Check reasoning field or try a different agent |
License
MIT
