Quickstart
Your first run
An API key from Settings, one install, one deploy, one run with a one-credit ceiling. The run keeps going after your process exits — that is the part worth seeing first.
Install the client
TypeScript · ESMThe SDK lives in packages/sdk-ts and is tested against every route on this page, but the npm package is not published yet. Until it is, the REST API below works from anywhere.
Registry status · preview
npm install @continuum/sdkAuthenticate
Settings → API keysKeys begin with cnt_ and are shown once. Keep them server-side and load them from your environment.
import { Continuum } from '@continuum/sdk';
const continuum = new Continuum({
apiKey: process.env.CONTINUUM_API_KEY!,
});Create, then deploy
immutable versionsAn agent is the stable identity. Each deploy creates a new immutable version; runs already in flight stay pinned to the version they started with.
const slug = 'research-sentinel';
await continuum.agents.create({
name: 'Research sentinel',
slug,
description: 'Checks a research queue and reports material changes.',
egressAllowlist: ['api.github.com'],
});
await continuum.agents.deploy(slug, {
model: 'gpt-5-mini',
system:
'Work in small, verifiable steps. End each turn with ' +
'@finish <result>, @sleep <seconds>, @approval <question>, ' +
'or @await <event>.',
maxOutputTokens: 2048,
maxIterations: 20,
});Start a run and watch it
SSE + pollingStreaming is optional and disconnecting never stops a run. The run lives in Continuum, not in the process that started it.
const run = await continuum.runs.start('research-sentinel', {
input: { goal: 'Review the release queue and flag breaking changes.' },
budgetCredits: 1,
});
for await (const event of continuum.runs.stream(run.id)) {
if (event.type === 'step') {
console.log(event.step.seq, event.step.kind, event.step.costMicro);
}
}
// No default timeout: this run may legitimately sleep for days.
const finished = await continuum.runs.wait(run.id);
console.log(finished.status, finished.output);Agent lifecycle
Every turn ends with a directive
The last line of an agent's reply decides what happens to its own compute: finish, sleep, ask a person, or wait for an event. Each directive maps to a status you can watch in the console.
@finish <result>completedFinish the run and persist the result.
@sleep <seconds>sleepingSuspend compute, arm a durable timer, and resume later.
@approval <question>waiting_approvalPark until an operator approves or rejects the proposed action.
@await <event>waiting_signalPark for an external signal. Deliver it with continuum.runs.sendEvent(runId, name, payload) — safe to call before the agent has finished parking.
Human in the loop
Answer an approval from any process
The live snapshot carries the pending question. Approval returns 202 because the workflow acts on the signal at its next safe point.
const snapshot = await continuum.runs.snapshot(run.id);
if (snapshot.pendingApproval) {
console.log(snapshot.pendingApproval.question);
await continuum.runs.approve(run.id, true, 'Reviewed by release lead');
}TypeScript SDK
The client, namespace by namespace
Five namespaces cover what you control from code: agent definitions, the runs themselves, what starts them, and what they spend.
continuum.agentsDefine and version what runs.
list()get(slug)create(input)delete(slug)versions(slug)deploy(slug, manifest)continuum.runsStart, inspect, stream, and control work.
start(slug, options)get(runId)list(params)steps(runId)snapshot(runId)stream(runId)wait(runId, options)pause(runId)resume(runId)approve(runId, approved, note)cancel(runId)continuum.triggersSchedules that start runs without anybody asking.
list(slug)create(slug, input)update(slug, id, changes)delete(slug, id)continuum.eventsEmit a name; every trigger listening for it starts a run.
emit(name, options)continuum.memoryWhat the agent keeps between runs.
list(slug)get(slug, key)set(slug, key, value)delete(slug, key)continuum.billingRead the workspace ledger and spend policy.
overview()Why wait() has no default timeout
timeoutMs only when your caller has a real deadline — it stops the wait, not the run.REST API
The same platform over HTTP
Everything the SDK does is one JSON request away, with stable error envelopes and server-sent trace streams. Four facts to check before writing the first one:
Authentication
x-api-key: cnt_…
Workspace pin
x-continuum-org: org_… (optional)
Money
String micro-credits · 1 credit = 1,000,000
Live traces
text/event-stream
curl -X POST https://api.openagents.cc/v1/agents/research-sentinel/runs \
-H 'x-api-key: $CONTINUUM_API_KEY' \
-H 'content-type: application/json' \
-d '{
"input": {"goal": "Review the release queue"},
"budgetCredits": 1,
"idempotencyKey": "release-review-2026-08-03"
}'Agents
| GET | /v1/agents | List agents in the active workspace |
| POST | /v1/agents | Create an agent |
| GET | /v1/agents/{slug} | Read one agent |
| DELETE | /v1/agents/{slug} | Soft-delete an agent |
| GET | /v1/agents/{slug}/versions | List immutable versions |
| POST | /v1/agents/{slug}/versions | Deploy and activate a version |
Triggers
| GET | /v1/agents/{slug}/triggers | List schedules and what each did last |
| POST | /v1/agents/{slug}/triggers | Add a schedule or a webhook URL |
| POST | /v1/agents/{slug}/triggers/{id}/rotate | Issue a new signing secret |
| POST | /triggers/{id}/webhook | Signed, unauthenticated — starts a run |
| POST | /v1/events | Emit a name; every listening trigger starts a run |
| PATCH | /v1/agents/{slug}/triggers/{id} | Pause, resume, or reschedule |
| DELETE | /v1/agents/{slug}/triggers/{id} | Remove a schedule |
Runs
| POST | /v1/agents/{slug}/runs | Start a budgeted run |
| GET | /v1/runs | List recent runs |
| GET | /v1/runs/{runId} | Read persisted run state |
| GET | /v1/runs/{runId}/snapshot | Query live workflow state |
| GET | /v1/runs/{runId}/steps | List trace steps in order |
| GET | /v1/runs/{runId}/stream | Stream steps over SSE |
| POST | /v1/runs/{runId}/approve | Answer a pending approval |
| POST | /v1/runs/{runId}/pause | Pause at the next safe boundary |
| POST | /v1/runs/{runId}/resume | Resume a paused run |
| POST | /v1/runs/{runId}/cancel | Signal cancellation |
Memory
| GET | /v1/agents/{slug}/memory | Every key, with usage against the plan quota |
| GET | /v1/agents/{slug}/memory/{key} | Read one entry |
| PUT | /v1/agents/{slug}/memory/{key} | Create or replace a key |
| DELETE | /v1/agents/{slug}/memory/{key} | Remove a key |
Billing
| GET | /v1/billing | Balance, plan, budget, and ledger activity |
Webhooks
| GET | /v1/webhooks | List registered endpoints |
| POST | /v1/webhooks | Register one; the signing secret is returned once |
| PATCH | /v1/webhooks/{id} | Change the URL, events, or enabled state |
| POST | /v1/webhooks/{id}/rotate | Issue a new signing secret |
| POST | /v1/webhooks/{id}/test | Send a ping and report what came back |
| GET | /v1/webhooks/{id}/deliveries | Read the delivery log |
| DELETE | /v1/webhooks/{id} | Remove an endpoint, keeping its history |
Run statuses
What each status tells you
A long run spends most of its life parked on purpose. The parked states keep the run's commitments — its timer, its question, its awaited event — without keeping a sandbox warm, so a sleeping run is a healthy run.
| Status | Class | |
|---|---|---|
queued | Live | Waiting for admission and provisioning |
provisioning | Compute | Creating or restoring the sandbox |
running | Compute | Executing an agent turn |
sleeping | Parked | Timer armed; compute suspended |
waiting_approval | Parked | Waiting for a human decision |
waiting_signal | Parked | Waiting for an external event |
paused | Parked | Stopped at a safe turn boundary |
completed | Terminal | Finished successfully |
failed | Terminal | Stopped because execution failed |
cancelled | Terminal | Stopped by an operator |
budget_exhausted | Terminal | A call would have crossed the run cap |
timed_out | Terminal | Maximum run lifetime reached |
Triggers
Starting runs without a person
A trigger is a standing instruction to spend money, so it is checked when you write it rather than when it fires. A schedule keeps its own clock; a webhook waits for somebody else's system; an event trigger listens for a name.
// Every weekday at 09:00 Paris time, daylight saving included.
await continuum.triggers.create('digest', {
cron: '0 9 * * 1-5',
timezone: 'Europe/Paris',
inputTemplate: { window: 'since-yesterday' },
});
// What each schedule did last, and why if it did nothing.
for (const trigger of await continuum.triggers.list('digest')) {
console.log(trigger.cron, trigger.nextFireAt, trigger.lastStatus, trigger.lastError);
}Or declare them alongside the agent
{
"name": "Digest",
"slug": "digest",
"manifest": { "model": "gpt-5-mini", "system": "..." },
"triggers": [
{ "cron": "0 9 * * 1-5", "timezone": "Europe/Paris" }
]
}continuum deploymakes the agent’s schedules match this list. Leave the key out entirely and your existing schedules are left alone — deploy runs far more often than schedules change.
What to expect
- Missed windows are skipped
- The next fire is always computed forward from now. If nothing was running for a day, a schedule does not start a day of catch-up runs — it starts the next one.
- A slow run blocks the next
- By default a schedule skips its turn while its previous run is still going, and records which run held it up. Runs here are long: a daily schedule over a three-week agent would otherwise stack twenty-one copies. Set overlapPolicy to "allow" if you want them concurrent.
- A schedule fires as nobody
- The run it starts has no user attached — it names the trigger instead. Nothing is emailed, so register a webhook if you want to hear about failures.
- Refusals are recorded, not hidden
- A blocked budget, a full concurrency limit or an agent with no deployed version leaves the schedule alive and lastStatus set to refused, with the reason. A schedule that stops firing can always say why.
- Your plan sets a floor
- Every fire starts a sandbox, so the shortest interval a schedule may use is capped per plan and checked when you save it rather than when it fires.
Webhook triggers: a URL for somebody else’s system
A webhook trigger has its own URL and its own signing secret. The secret is returned once, at creation and on rotation — it is sealed at rest, so losing it means rotating rather than recovering.
// The secret comes back here and nowhere else.
const trigger = await continuum.triggers.create('digest', { type: 'webhook' });
console.log(trigger.webhookUrl); // https://api.openagents.cc/triggers/trg_…/webhook
console.log(trigger.secret); // whsec_… store this nowSigning a request
import { createHmac } from 'node:crypto';
const body = JSON.stringify({ orderId: 4021 });
const t = Math.floor(Date.now() / 1000);
// The signature covers `${t}.${body}` — the timestamp is inside the MAC, so a
// captured request cannot be replayed once it falls outside the window.
const v1 = createHmac('sha256', process.env.TRIGGER_SECRET!)
.update(`${t}.${body}`)
.digest('hex');
await fetch(webhookUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
'Continuum-Signature': `t=${t},v1=${v1}`,
},
body, // the exact bytes that were signed
});401 is deliberately vague
- Sign the raw bytes
- The signature covers the body exactly as sent. Parsing and re-serialising reorders keys and reformats numbers, and a verifier built on a parsed body works until the day your JSON library changes.
- Retries are free
- The run id is derived from the signed request, so a timeout-and-retry of the identical payload returns the same run rather than starting a second. That is also what makes a replay inside the five-minute window harmless.
- One answer for every rejection
- A missing signature, a wrong secret, an altered body and an expired timestamp all return 401, and an unknown trigger 404s exactly like a deleted one. Telling them apart would help an attacker more than it helps you.
- 409 means it did not start
- By default a webhook trigger will not start a run while its previous one is still going, and says which run held it up. Set overlapPolicy to "allow" for concurrent runs.
- The body merges over the template
- Whatever you post is layered on top of the trigger’s inputTemplate and becomes the run input. Bodies are capped at 256 KB.
Event triggers: one name, every listener
An event trigger listens for a name emitted anywhere in your workspace. Several agents may answer the same name, so an emit fans out — and a fan-out where one agent is busy and another is undeployed is ordinary, not a failure.
// Two agents can listen for the same name, and both will start.
await continuum.triggers.create('fulfilment', {
type: 'event',
eventName: 'order.paid',
});
const result = await continuum.events.emit('order.paid', {
payload: { orderId: 4021 },
});
// Fan-out means partial success is ordinary — read the results, not the status.
console.log(`${result.started}/${result.matched} started`);
for (const r of result.results) {
if (r.outcome !== 'started') console.warn(r.triggerId, r.outcome, r.reason);
}Two endpoints, one word
- POST /v1/events — starts
- Emits a name into the workspace. Every enabled event trigger listening for it starts a new run. Nothing needs to be running first, and matching zero triggers is not an error.
- POST /v1/runs/{runId}/events — resumes
- Wakes one run that is already parked on @await. It never starts anything, and it does nothing if no run is waiting for that name.
- They do not chain
- Emitting a name will not wake a run parked on it, and a trigger whose previous run is that parked run will skip rather than start a second copy beside it.
Code tools
Tools written as TypeScript functions
A declared HTTP tool is one request the manifest spells out. A code tool is your own function — it can parse a response, branch on it, call two APIs and reconcile them, and use any package you have installed.
npm install @continuum/agentimport { defineAgent, tool } from '@continuum/agent';
import { z } from 'zod';
export default defineAgent({
model: 'gpt-5-mini',
system: 'You handle refunds. End every turn with a directive.',
tools: [
tool({
name: 'refundOrder',
// The model picks tools by this sentence alone.
description: 'Refunds an order and returns the refund id.',
parameters: z.object({ orderId: z.string() }),
async run({ orderId }, ctx) {
const res = await fetch('https://api.stripe.com/v1/refunds', {
method: 'POST',
headers: { authorization: `Bearer ${ctx.secrets.STRIPE_KEY}` },
body: new URLSearchParams({ charge: orderId }),
});
return await res.json();
},
}),
],
});Point the project file at it
{
"name": "Refund desk",
"slug": "refund-desk",
"entry": "agent.ts",
"manifest": {
"model": "gpt-5-mini",
"system": "You handle refunds."
}
}Leave entry out and nothing changes — the manifest deploys exactly as it always has. continuum init --code writes both files for you.
$ continuum deploy
✓ bundled agent.ts → 412.3 KB (18 modules)
✓ uploaded sha256:9f2c1a4e7b03… (new)
✓ 1 tool: refundOrder
✓ deployed refund-desk v7 (active)Egress stops being something we can check
What to expect
- Egress is not inferred any more
- A declared HTTP tool names its host, so a deploy can check it. A function chooses at runtime, so nothing can. Your tools reach only what the agent’s egressAllowlist permits, and anything else is dropped rather than refused — which presents as a timeout, not an error. Deploy says so when a version carries code tools.
- Bundled on your machine
- continuum deploy runs esbuild locally and uploads one file, so every package you have installed is inlined before it leaves. The sandbox installs nothing and reaches no registry.
- Secrets arrive on ctx, never process.env
- Your tool runs in a process whose environment is built from nothing, so it holds no Continuum values at all — not this sandbox’s own token, and not your secrets. ctx.secrets is the only way in, and the only one that cannot also hand out platform credentials.
- Throwing is an answer
- A tool that throws returns that error to the model, which can try something else. It does not fail the run — killing a three-week agent because an upstream API had a bad minute would be the wrong trade every time.
- One file, addressed by its hash
- A bundle is stored under the sha-256 of its bytes, so redeploying unchanged code uploads nothing and a run that started three weeks ago still boots exactly what it was deployed with.
- Bounded like any other tool
- A tool is stopped at timeoutMs (30s by default) and its result capped at maxResultBytes (64 KB). A tool returning a 40 MB object would be re-billed as input tokens on every later turn.
Webhooks
Hearing about runs nobody is watching
A run started by an API key has no person behind it to email. Register an endpoint in Settings and your own systems hear about every run that finishes, fails, or parks on a decision.
import { parseWebhook } from '@continuum/sdk';
export async function POST(request: Request) {
// The raw bytes, not a parsed body: re-serializing JSON reorders keys
// and the signature covers the exact bytes we sent.
const body = await request.text();
try {
const event = await parseWebhook({
body,
header: request.headers.get('continuum-signature'),
secret: process.env.CONTINUUM_WEBHOOK_SECRET!,
});
// At-least-once: the same event id can arrive more than once.
// Deduplicate on it before doing anything with a side effect.
if (await alreadyHandled(event.id)) return new Response(null, { status: 200 });
if (event.type === 'run.completed') {
await onRunFinished(event.data.run);
}
return new Response(null, { status: 200 });
} catch {
// Not ours, or too old. A rotated secret looks like this too.
return new Response(null, { status: 400 });
}
}Events
- run.completed
- The run finished successfully.
- run.failed
- The run stopped because execution failed.
- run.cancelled
- Somebody cancelled the run.
- run.timed_out
- The run reached its maximum lifetime.
- run.budget_exhausted
- A call would have crossed the run cap.
- run.waiting_approval
- The run is parked on a human decision.
- ping
- A test event, sent only when you ask for one.
What to expect
- Signature
- HMAC-SHA256 over `${timestamp}.${rawBody}`, sent as `Continuum-Signature: t=…,v1=…`. Five minutes of clock tolerance.
- Delivery
- At least once. Retried seven times over about two and a half hours on a 5xx, a timeout, or a 429 — never on a 4xx.
- Duplicates
- `Continuum-Event-Id` is stable across retries and identical for every endpoint. Deduplicate on it.
- Timeouts
- Ten seconds to answer. Acknowledge first and do the work afterwards.
- Redirects
- Not followed. An endpoint that moved needs its URL updated.
Errors and retries
Handling failure without guessing
Error messages are prose and may be reworded; codes are the contract your integration branches on. Request IDs connect your logs to the platform's trace of the same request.
import { ContinuumApiError } from '@continuum/sdk';
try {
await continuum.runs.start('research-sentinel', { input: {} });
} catch (error) {
if (error instanceof ContinuumApiError) {
console.error({
code: error.code,
status: error.status,
retryable: error.retryable,
requestId: error.requestId,
});
}
}unauthorizedThe key is missing, invalid, or revoked.forbidden / plan_limit_exceededThe actor or plan cannot perform this operation.conflictState changed or the requested transition is not currently valid.validation_failedThe request shape is invalid; inspect error.details.retryable: trueBack off and retry with the same idempotency key.Next
Five credits are already on the house
Sign up, deploy an agent against the mock: models, and walk its whole lifecycle — runs, sleeps, approvals, the trace — before connecting a production model or a card.