JavaScript SDK
@cestra/sdk runs on Node 18 and newer and has zero dependencies. It ships with TypeScript types. run() receives events over a WebSocket on Node 22+, and falls back to REST long-polling elsewhere.
npm install https://docs.cestra.ai/dl/cestra-sdk.tgzimport { Agent } from '@cestra/sdk';
const agent = new Agent({ key: process.env.CESTRA_KEY });
await agent.send('Hello from Cestra');new Agent() with no options reads CESTRA_KEY and CESTRA_URL from the environment.
| Option | Default | |
|---|---|---|
key |
process.env.CESTRA_KEY |
Your cs_live_ key |
baseUrl |
process.env.CESTRA_URL or https://api.cestra.ai |
|
maxRetries |
4 |
Retries for 429, 5xx and network errors |
timeoutMs |
30000 |
Per request. Long-polls add their wait time |
transport |
'auto' |
'websocket', 'poll' or 'auto' (WebSocket, then long-polling) |
#Messages and notices
await agent.send('Build **passed** in 3m 12s'); // Markdown
await agent.notice('Backup done', 'success'); // info | success | warning | error
await agent.send('Quiet update', { notify: false }); // no push notification#Asks
const ask = await agent.ask('Commit to main?', ['Approve', { label: 'Discard', style: 'destructive' }], {
expiresIn: 3600, // seconds, 60 s … 30 days
allowText: false, // true adds a text box
detail: '3 files changed',
});
const reply = await ask.wait({ timeoutSec: 600 });
// { status: 'answered' | 'pending' | 'expired' | 'cancelled', answer, optionId, text, answeredBy, answeredAt }
if (reply.optionId === 'o1') { /* approved */ }Options get ids o1, o2, … in order. wait() never throws on timeout. It returns the ask with status: 'pending'. Call ask.refresh() or ask.cancel() later. To pick an ask back up after a restart, use agent.getAsk(id).
#Jobs
const job = await agent.job('Reviewing spec', ['Read', 'Compare', 'Write up']);
await job.progress(40);
await job.step(0, 'done');
await job.step(1, 'running');
await job.done(); // or: await job.fail('Spec file missing')job.update({ title, pct, status, steps, step, error }) is the raw form. When someone presses Stop, you get a job.stop_requested event, and later updates fail with a 409 because the job is now cancelled.
#Artifacts
await agent.artifact({
type: 'table',
title: 'Coverage matrix',
key: 'matrix', // same key again = version 2, 3, …
data: { columns: ['Requirement', 'Status'], rows: [['Login', { value: 'ok', tone: 'ok' }]] },
});Schemas for table, chart, markdown, form and html are on Artifacts.
#Files
await agent.file('report.pdf', { caption: 'Quote' }); // path, Buffer, Uint8Array or Blob, ≤ 50 MB
const bytes = await agent.download(fileId); // Buffer
const { name, mime, data } = await agent.downloadFile(fileId);#Receiving events
agent.on('message', async (e) => {
if (e.data.file) await agent.download(e.data.file.id);
else await agent.send(`You said: ${e.data.text}`);
});
agent.on('ask.answered', (e) => console.log(e.data.ask_id, e.data.answer));
agent.on('job.stop_requested', (e) => stopWork(e.data.job_id));
agent.on('*', (e) => console.log(e.seq, e.type));
await agent.run(); // resolves after agent.stop()run() acknowledges each event after its handlers resolve, so delivery is at-least-once. It reconnects with backoff. It rejects with a CestraError (status 401) if the key is invalid or revoked. on() returns an unsubscribe function. There are also lifecycle events: connected, disconnected and error.
Event types and payloads are listed under REST API → Events.
#Errors
Everything throws CestraError with status, code and message. Creates carry an automatic clientId, so retries never duplicate anything. Pass your own clientId to stay idempotent across restarts.
#Other calls
agent.me(), agent.setStatus('working' | 'online'), agent.disconnect(), agent.events({ after, wait }), agent.ack(seq), agent.jobById(id).