Documentation · SDK 0.1.0 · API 0.1.0

How to run an agent for three weeks.

Install the client, deploy a version, start a run with a budget, and read what it did — including the days it spent asleep. Every example on this page is written against the shipped TypeScript client and REST routes.

SDK
0.1.0 · source preview
Runtime
Node.js · fetch-compatible
Auth
x-api-key · cnt_…
Base URL
api.openagents.cc
Status
api.openagents.cc
On this page

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.

01

Install the client

TypeScript · ESM

The 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

shellpublish target
npm install @continuum/sdk
02

Authenticate

Settings → API keys

Keys begin with cnt_ and are shown once. Keep them server-side and load them from your environment.

typescriptclient.ts
import { Continuum } from '@continuum/sdk';

const continuum = new Continuum({
  apiKey: process.env.CONTINUUM_API_KEY!,
});
03

Create, then deploy

immutable versions

An agent is the stable identity. Each deploy creates a new immutable version; runs already in flight stay pinned to the version they started with.

typescriptdeploy.ts
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,
});
04

Start a run and watch it

SSE + polling

Streaming is optional and disconnecting never stops a run. The run lives in Continuum, not in the process that started it.

typescriptrun.ts
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>completed

Finish the run and persist the result.

@sleep <seconds>sleeping

Suspend compute, arm a durable timer, and resume later.

@approval <question>waiting_approval

Park until an operator approves or rejects the proposed action.

@await <event>waiting_signal

Park 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.

typescriptapproval.ts
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.agents

Define and version what runs.

list()get(slug)create(input)delete(slug)versions(slug)deploy(slug, manifest)
continuum.runs

Start, 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.triggers

Schedules that start runs without anybody asking.

list(slug)create(slug, input)update(slug, id, changes)delete(slug, id)
continuum.events

Emit a name; every trigger listening for it starts a run.

emit(name, options)
continuum.memory

What the agent keeps between runs.

list(slug)get(slug, key)set(slug, key, value)delete(slug, key)
continuum.billing

Read the workspace ledger and spend policy.

overview()

Why wait() has no default timeout

A healthy run may sleep for a week. A short library timeout would train every integration to build unnecessary retry loops. Pass 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

curlstart a run
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

Agents endpoints
GET/v1/agents
POST/v1/agents
GET/v1/agents/{slug}
DELETE/v1/agents/{slug}
GET/v1/agents/{slug}/versions
POST/v1/agents/{slug}/versions

Triggers

Triggers endpoints
GET/v1/agents/{slug}/triggers
POST/v1/agents/{slug}/triggers
POST/v1/agents/{slug}/triggers/{id}/rotate
POST/triggers/{id}/webhook
POST/v1/events
PATCH/v1/agents/{slug}/triggers/{id}
DELETE/v1/agents/{slug}/triggers/{id}

Runs

Runs endpoints
POST/v1/agents/{slug}/runs
GET/v1/runs
GET/v1/runs/{runId}
GET/v1/runs/{runId}/snapshot
GET/v1/runs/{runId}/steps
GET/v1/runs/{runId}/stream
POST/v1/runs/{runId}/approve
POST/v1/runs/{runId}/pause
POST/v1/runs/{runId}/resume
POST/v1/runs/{runId}/cancel

Memory

Memory endpoints
GET/v1/agents/{slug}/memory
GET/v1/agents/{slug}/memory/{key}
PUT/v1/agents/{slug}/memory/{key}
DELETE/v1/agents/{slug}/memory/{key}

Billing

Billing endpoints
GET/v1/billing

Webhooks

Webhooks endpoints
GET/v1/webhooks
POST/v1/webhooks
PATCH/v1/webhooks/{id}
POST/v1/webhooks/{id}/rotate
POST/v1/webhooks/{id}/test
GET/v1/webhooks/{id}/deliveries
DELETE/v1/webhooks/{id}
Browse the full OpenAPI schema

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.

StatusClass
queuedLive
provisioningCompute
runningCompute
sleepingParked
waiting_approvalParked
waiting_signalParked
pausedParked
completedTerminal
failedTerminal
cancelledTerminal
budget_exhaustedTerminal
timed_outTerminal

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.

typescriptschedule.ts
// 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

jsoncontinuum.json
{
  "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.

typescriptcreate.ts
// 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 now

Signing a request

typescriptsend.ts
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

A missing signature, a wrong secret, an altered body and an expired timestamp are one answer, and an unknown trigger 404s exactly like a deleted one.
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.

typescriptevents.ts
// 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

Emitting an event starts runs. Sending an event to a run wakes one. They are different verbs and they do not chain.
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.

shellinstall
npm install @continuum/agent
typescriptagent.ts
import { 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

jsoncontinuum.json
{
  "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.

shelldeploy
$ 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

A declared tool names its host, so a deploy verifies it. A function decides at runtime, so nothing can. Your code reaches only what the agent’s allowlist permits, and everything else is dropped rather than refused — which looks like a slow API, not like a policy. Deploy says so out loud when a version carries code tools.

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.

typescriptroute.ts
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.

typescripterrors.ts
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,
    });
  }
}
401unauthorizedThe key is missing, invalid, or revoked.
403forbidden / plan_limit_exceededThe actor or plan cannot perform this operation.
409conflictState changed or the requested transition is not currently valid.
422validation_failedThe request shape is invalid; inspect error.details.
429/5xxretryable: 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.