> ## Documentation Index
> Fetch the complete documentation index at: https://docs.antigen.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK reference

> Build, observe, recover, and control authorized Antigen Runs from TypeScript.

`@antigenco/sdk` is the typed client for Antigen Runs. It offers two entry points into the same
API. Use `antigen.agent(config).run(targets)` when the model and Guardrails belong together as one
assessment configuration. Use `antigen.runs` when your application owns the request fields, or when
it is reconnecting to a Run it created earlier.

An integration owns three things: an organization API key, an approved target, and durable storage
for the Run ID. Antigen supplies the approved target list; you choose from it and record why the
target is in scope. A Run ID is a locator rather than a credential, so store it alongside the
assessment record and rely on organization permissions for every later read or operation.

## Install

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @antigenco/sdk
```

The SDK is ESM-only and requires Node 22 or later. It ships its own TypeScript declarations.

## Authenticate

Pass an organization API key as the first argument. Keys begin with `atg_sk_`; read them from your
secret source at startup and never commit or log them.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Antigen from "@antigenco/sdk";

const apiKey = process.env.ANTIGEN_API_KEY;
if (!apiKey) throw new Error("Set ANTIGEN_API_KEY to an organization API key.");

const antigen = new Antigen(apiKey);
```

The `atg_sk_` prefix identifies the credential kind, not its validity, expiry, or scope, so a shape
check cannot tell you whether a key still works. The service answers that with a structured error
carrying a `requestId`.

Later examples on this page assume this `antigen` client and an approved `target` string already
exist, so that each one shows only the call being described.

## Quick start

This complete program picks an approved target, creates a Run, follows its events, and prints the
first finding's remediation. Save it as `assess.ts` and run it with `npx tsx assess.ts`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { writeFile } from "node:fs/promises";
import Antigen from "@antigenco/sdk";

const apiKey = process.env.ANTIGEN_API_KEY;
if (!apiKey) throw new Error("Set ANTIGEN_API_KEY to an organization API key.");

const antigen = new Antigen(apiKey);
const approved = await antigen.getApprovedTargets();
const target = approved.find(
  (candidate) => candidate === process.env.ANTIGEN_TARGET,
);
if (!target)
  throw new Error(`Set ANTIGEN_TARGET to one of: ${approved.join(", ")}`);

const run = await antigen
  .agent({
    model: "claude-sonnet-4-6",
    guardrails: "Assess only the approved target.",
  })
  .run([target]);
await writeFile(".antigen-run-id", `${run.id}\n`, { mode: 0o600 });

for await (const event of run) {
  console.log(`${event.sequence}: ${event.summary}`);
  await writeFile(".antigen-event-position", `${event.sequence}\n`, {
    mode: 0o600,
  });
}

const [finding] = await run.findings();
console.log(
  finding
    ? `Next action: ${finding.remediation}`
    : "Completed with no findings.",
);
```

The two files stand in for whatever durable storage your application already operates. The Run ID
must be written as soon as creation succeeds, and an event sequence only after that event has been
handled. Those two ordering rules are what make recovery deterministic.

## Client

### new Antigen

Constructs a client. The common form takes an API key; the object form is for callers that hold a
user access token or need to reach a non-hosted environment.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
new Antigen(apiKey: string, options?: Omit<AntigenOptions, "accessToken" | "apiKey">)
new Antigen(options: AntigenOptions)
```

| Option        | Type           | Description                                                                                              |
| ------------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| `apiKey`      | `string`       | An organization API key beginning with `atg_sk_`. Sent as `x-api-key`.                                   |
| `accessToken` | `string`       | A user access token, used instead of an API key. Sent as `authorization: Bearer`.                        |
| `baseUrl`     | `string`       | API origin. Defaults to `https://api.antigen.sh`. Use it for local development and Preview environments. |
| `fetch`       | `typeof fetch` | A replacement `fetch` implementation, for proxies or instrumentation.                                    |

The object form is the one to reach for when a value other than an API key varies:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const antigen = new Antigen({
  accessToken: userAccessToken,
  baseUrl: process.env.ANTIGEN_BASE_URL ?? "https://api.antigen.sh",
});
```

Pass exactly one of `apiKey` and `accessToken`. Supplying both, neither, a blank credential, or an
`apiKey` without the `atg_sk_` prefix throws `AntigenProtocolError` from the constructor. `baseUrl`
is validated the same way: it must be an `http` or `https` URL carrying no query, fragment, or
embedded credentials.

The client exposes `antigen.runs` for the Run collection and `antigen.agent(config)` for the
assessment facade. Constructing a client performs no network request, so a well-formed but invalid
credential is reported by the service on the first call, not here.

### antigen.getApprovedTargets

Lists the targets approved for the caller's organization. Call it before creating a Run.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.getApprovedTargets(options?: RequestOptions): Promise<string[]>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const targets = await antigen.getApprovedTargets();
if (targets.length === 0)
  throw new Error("No approved targets for this organization.");
```

Select a returned value unchanged. If creation later returns `TARGET_NOT_APPROVED`, read this list
again rather than editing the rejected target. An empty list is an authorization question for your
organization administrator, not an integration bug.

## Create a Run

### antigen.agent

Creates an assessment facade that keeps model configuration and Guardrails next to Run creation.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.agent(config: AgentConfig): Agent
```

| Field        | Type                | Description                                                                          |
| ------------ | ------------------- | ------------------------------------------------------------------------------------ |
| `model`      | `string`            | Required. The assessment model.                                                      |
| `guardrails` | `string`            | Natural-language scope guidance sent with the Run.                                   |
| `skills`     | `readonly string[]` | Local paths to Markdown files or directories, read into the request at `run()` time. |

Each `skills` entry may be a `.md` file or a directory, which is walked in deterministic order.
Symbolic links are rejected, empty files are rejected, and each Skill's name is its filename without
the extension, so names must be unique across the whole set. Treat these files as reviewed
application input and keep credentials out of them.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const agent = antigen.agent({
  model: "claude-sonnet-4-6",
  guardrails: "Assess only the approved target.",
});
```

Guardrails shape how the agent works. The approved target list is the enforced boundary. Keep one
assessment configuration to one coherent objective.

### agent.run

Creates a Run from the Agent's configuration for one or more approved targets.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent.run(targets: readonly string[], options?: RequestOptions): Promise<Run>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const run = await agent.run([target]);
await writeFile(".antigen-run-id", `${run.id}\n`, { mode: 0o600 });
```

`targets` must be non-empty and every entry must come from `getApprovedTargets`. Invalid input,
transport failures, and service rejections reject rather than returning a partial Run, so a
resolved promise always carries a real `run.id`.

Persist `run.id` immediately. If the create response is ambiguous, read that saved ID with
[`runs.get`](#runs-get) before deciding whether another assessment is necessary. A second create is a
new assessment record, never a retry.

### runs.create

Creates a Run from request fields your application supplies directly. Use this form when the
application, rather than an Agent configuration, owns the request.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.runs.create(input: CreateRunRequest, options?: RequestOptions): Promise<Run>
```

| Field        | Type         | Description                                                               |
| ------------ | ------------ | ------------------------------------------------------------------------- |
| `model`      | `string`     | Required. The assessment model.                                           |
| `targets`    | `string[]`   | Required, non-empty. Every entry from `getApprovedTargets`.               |
| `guardrails` | `string`     | Natural-language scope guidance.                                          |
| `skills`     | `RunSkill[]` | Inline Skills as `{ name, content }` pairs, already loaded by the caller. |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const run = await antigen.runs.create({
  model: "claude-sonnet-4-6",
  targets: [target],
  guardrails: "Assess only the approved target.",
});
```

Unlike `agent.run`, this form takes Skill content inline rather than reading local paths, so it
works in runtimes without filesystem access. Pick one creation form per integration boundary.

## Find a Run

| Method                    | Use it when                                | Result              |
| ------------------------- | ------------------------------------------ | ------------------- |
| [`runs.list`](#runs-list) | You need recent Runs or a filtered page.   | `Promise<RunsPage>` |
| [`runs.get`](#runs-get)   | You are reconnecting to a saved opaque ID. | `Promise<Run>`      |

### runs.list

Reads a page of Runs visible to the caller's organization.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.runs.list(input?: ListRunsRequest, options?: RequestOptions): Promise<RunsPage>
```

| Field    | Type        | Description                                        |
| -------- | ----------- | -------------------------------------------------- |
| `limit`  | `number`    | Page size, 1 through 100. Defaults to 50.          |
| `cursor` | `string`    | An opaque continuation value from a previous page. |
| `status` | `RunStatus` | Restrict the page to one lifecycle status.         |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const page = await antigen.runs.list({ limit: 50 });
for (const run of page.runs) console.log(run.id, run.current.status);
if (page.nextCursor) console.log(`More: ${page.nextCursor}`);
```

`runs` are full `Run` objects, so you can act on one without a second read. Preserve `nextCursor`
unchanged and pass it back only for the next page. Do not derive authorization, identity, or
ordering from a cursor or a Run ID.

### runs.get

Reads a known Run by its saved opaque ID. This is the entry point for every recovery path.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.runs.get(runId: string, options?: RequestOptions): Promise<Run>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const runId = (await readFile(".antigen-run-id", "utf8")).trim();
const run = await antigen.runs.get(runId);
console.log(run.current.status);
```

`RUN_NOT_FOUND` and authorization failures reject: the ID alone does not grant access. Read a saved
Run before retrying any ambiguous create or lifecycle request.

## Observe a Run

A `Run` separates the durable assessment record from what your caller has observed. `run.current`
holds the last representation the client saw, without making a request. The methods below refresh
it. Process evidence before advancing its saved checkpoint.

### run.status

Reads and refreshes the latest durable representation of the Run.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.status(options?: RequestOptions): Promise<RunState>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
console.log((await run.status()).status);
```

`RunState` is a discriminated union on `status`, so the timestamps it carries depend on how far the
Run has progressed: `startedAt` once running, `stoppedAt` when stopped, `completedAt` on a terminal
state, and a `failure` object of `{ code, message }` when `status` is `failed`.

Read status after an ambiguous create, a lifecycle action, a local wait, or an event interruption,
before deciding on the next operation.

### run.events

Streams ordered events and resumes cleanly after a handled checkpoint.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.events(options?: EventOptions): AsyncGenerator<RunEvent>
```

| Option  | Type     | Description                                                                               |
| ------- | -------- | ----------------------------------------------------------------------------------------- |
| `after` | `number` | Resume strictly after this processed event sequence. Must be a non-negative safe integer. |

A `Run` is itself async-iterable, so `for await (const event of run)` is shorthand for
`run.events()` with no options.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const saved = await readFile(".antigen-event-position", "utf8").catch(() => "");
const after = saved.trim() === "" ? undefined : Number(saved.trim());

for await (const event of run.events({ after })) {
  console.log(event.summary);
  await writeFile(".antigen-event-position", `${event.sequence}\n`, {
    mode: 0o600,
  });
}
```

Every event carries `sequence`, `summary`, `timestamp`, and an optional originating `agent` of
`planner`, `recon`, `executor`, or `system`. Switch on `type` for the rest:

| `type`                         | Additional fields                | Meaning                                                               |
| ------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
| `thinking`, `tool`, `progress` | n/a                              | Ordered progress narration.                                           |
| `finding`                      | `findingId`, `severity`, `title` | A finding was recorded; read it with [`run.findings`](#run-findings). |
| `lifecycle`                    | `from`, `to`                     | The Run changed status. A terminal `to` ends the stream.              |
| `error`                        | `error`                          | Execution failed, with a `{ code, message }` failure.                 |

The SDK reconnects a dropped stream on your behalf and recognizes exact duplicate replay within the
most recent 256 events. Out-of-order or unknown older events are protocol errors rather than silent
gaps. Persist `event.sequence` only after handling the event; saving first can skip unprocessed
work, and never saving makes the same evidence look new after every interruption.

### run.findings

Reads durable findings for the Run.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.findings(options?: RequestOptions): Promise<Finding[]>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for (const finding of await run.findings()) {
  console.log(`${finding.severity}: ${finding.title}`);
  console.log(finding.remediation);
}
```

| Field                  | Type                                                  | Description                                                    |
| ---------------------- | ----------------------------------------------------- | -------------------------------------------------------------- |
| `id`, `runId`          | `string`                                              | Identity, and the Run this finding belongs to.                 |
| `severity`             | `"critical" \| "high" \| "medium" \| "low" \| "info"` | Assessed severity.                                             |
| `title`, `description` | `string`                                              | What was found.                                                |
| `remediation`          | `string`                                              | The documented starting point for the next action.             |
| `proof`                | `{ request?, response? }`                             | Supporting evidence, when available.                           |
| `endpoint`, `cwe`      | `string`                                              | The affected endpoint and CWE classification, when applicable. |
| `createdAt`            | `string`                                              | When the finding was recorded.                                 |

A completed Run can have no findings; that is a valid assessment result, not an integration
failure. Treat findings as evidence for a human risk decision, and preserve `proof` and
`remediation` with the assessment record. A local failure does not mean a finding was removed.

### run.wait

Waits locally until the Run reaches `completed`, `failed`, or `cancelled`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.wait(options?: WaitOptions): Promise<RunState>
```

| Option           | Type     | Description                                                      |
| ---------------- | -------- | ---------------------------------------------------------------- |
| `waitTimeoutMs`  | `number` | Total local wait deadline. Defaults to 1,800,000 (30 minutes).   |
| `pollIntervalMs` | `number` | Poll interval. Defaults to 1,000, clamped to 100 through 30,000. |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Antigen, { AntigenWaitTimeoutError } from "@antigenco/sdk";

try {
  console.log((await run.wait({ waitTimeoutMs: 300_000 })).status);
} catch (error) {
  if (!(error instanceof AntigenWaitTimeoutError)) throw error;
  console.log("Local wait ended; reconnect to the saved Run later.");
}
```

`stopped` is not wait-terminal, so a stopped Run keeps the wait running until the deadline. The
deadline is local: on `AntigenWaitTimeoutError`, reconnect with [`runs.get`](#runs-get) and
[`run.events`](#run-events) rather than assuming the Run ended or calling `run.cancel`.

## Guide a Run

### run.send

Delivers scoped guidance to an assessment already under way, without changing its approved purpose.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.send(content: string, options?: RequestOptions): Promise<SendRunMessageResponse>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const delivery = await run.send(
  "Continue only within the approved target and Guardrails.",
);
console.log(`Delivered ${delivery.id} at ${delivery.deliveredAt}`);
```

`content` must be non-empty and stay inside the approved target and Guardrails. Prefer a specific,
reviewable request over a broad change of purpose. If necessary work falls outside the approved
scope, obtain authorization and create a separate assessment instead.

## Control a Run

`completed`, `failed`, and `cancelled` are terminal. Use a lifecycle operation to express a
decision, not to repair a local timeout. If one is rejected with `INVALID_RUN_STATE`, another
transition already occurred: read the Run and choose an action valid for the observed state.

### run.stop

Requests a reviewable pause, leaving the Run resumable.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.stop(options?: RequestOptions): Promise<RunState>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
console.log((await run.stop()).status);
```

### run.resume

Requests continuation of a stopped Run you already hold.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.resume(options?: RequestOptions): Promise<RunState>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
console.log((await run.resume()).status);
```

Resuming does not re-evaluate target approval, so confirm the approved scope still applies first.

### runs.resume

Resumes a stopped Run by ID and returns a `Run` object for it. Use this when the application has a
saved ID but not a live `Run`, which is the usual case after a restart.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
antigen.runs.resume(runId: string, options?: RequestOptions): Promise<Run>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const run = await antigen.runs.resume(runId);
for await (const event of run.events({ after })) console.log(event.summary);
```

`agent.resume(runId, options)` is an equivalent alias on the Agent facade, for code that is already
working through an Agent. It ignores the Agent's model and Guardrails, because a resumed Run keeps
the configuration it was created with.

### run.cancel

Ends the assessment. `cancelled` is terminal.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
run.cancel(options?: RequestOptions): Promise<RunState>
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
console.log((await run.cancel()).status);
```

Cancel only when ending the assessment is the intended business decision, never as a response to a
local timeout or a dropped connection.

## Request options

Every method accepts `RequestOptions` as its last argument. Both fields bound the caller's own work
and neither changes a Run that Antigen has already accepted.

| Option             | Type          | Description                           |
| ------------------ | ------------- | ------------------------------------- |
| `signal`           | `AbortSignal` | Cancels the caller's local operation. |
| `requestTimeoutMs` | `number`      | Bounds one HTTP request.              |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type { RequestOptions } from "@antigenco/sdk";

const options: RequestOptions = { requestTimeoutMs: 30_000 };
console.log(await antigen.getApprovedTargets(options));
```

Use a short request deadline for interactive actions and one that fits your job queue for
background work. `run.wait` extends these with `waitTimeoutMs` and `pollIntervalMs`, and
`run.events` extends them with `after`.

## Errors

The SDK distinguishes local wait timeouts, protocol failures, and HTTP failures. Transport failures
surface as the underlying `fetch` error.

| Class                     | Thrown when                                                   | Useful fields                 |
| ------------------------- | ------------------------------------------------------------- | ----------------------------- |
| `AntigenHttpError`        | The service returned a non-success status.                    | `status`, `requestId`, `body` |
| `AntigenProtocolError`    | A response, event ordering, or local Skill file was unusable. | `message`                     |
| `AntigenWaitTimeoutError` | `run.wait` reached its local deadline.                        | `timeoutMs`                   |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Antigen, { AntigenHttpError } from "@antigenco/sdk";

try {
  await antigen.runs.create({ model: "claude-sonnet-4-6", targets: [target] });
} catch (error) {
  if (!(error instanceof AntigenHttpError)) throw error;
  console.error(`HTTP ${error.status} requestId=${error.requestId ?? "none"}`);
}
```

A structured service error carries a stable `code` in its body. Retain the `requestId` with the
saved Run ID: it lets support identify the failing request without exposing credentials.

| Code                    | HTTP  | Meaning                                                                          |
| ----------------------- | ----- | -------------------------------------------------------------------------------- |
| `INVALID_REQUEST`       | `400` | The request body or a path value failed validation.                              |
| `TARGET_NOT_APPROVED`   | `400` | A submitted target is not approved. `details.targets` lists the rejected values. |
| `UNAUTHENTICATED`       | `401` | No recognizable credential was presented.                                        |
| `FORBIDDEN`             | `403` | The credential is valid but lacks the required scope.                            |
| `RUN_NOT_FOUND`         | `404` | No Run with that ID exists within what the credential can see.                   |
| `NOT_FOUND`             | `404` | The request path matches no endpoint. Usually a `baseUrl` mistake.               |
| `INVALID_RUN_STATE`     | `409` | The Run's current status does not accept that action.                            |
| `EXECUTION_UNAVAILABLE` | `503` | Execution capacity is temporarily unavailable.                                   |
| `INTERNAL`              | `500` | An unexpected service failure. Retain the `requestId`.                           |

`RUN_NOT_FOUND` also answers a Run held by another organization, so it is not evidence that an ID was
never issued. Branch on `status` for a `code` you do not recognize rather than matching the list
exhaustively: the set can grow, and an unhandled code should not become a crash.

`AntigenHttpError` is worth retrying when its `status` is `408`, `429`, or `5xx`, and is not worth
retrying otherwise. Creating a Run is the exception to retrying at all: there is no idempotency key,
so a retried create that in fact succeeded leaves two Runs executing and billable. List Runs first.

## Recover from an ambiguous result

When a create, lifecycle, or observation call fails, the local failure tells you what your caller
knows. It does not establish what the Run did. Recovery is always the same shape:

1. Read the saved Run ID from your durable store.
2. Call [`runs.get`](#runs-get), then [`run.status`](#run-status) for its current state.
3. Continue [`run.events`](#run-events) strictly after the last sequence you finished handling.
4. Read [`run.findings`](#run-findings) for durable remediation work.

Do not send a replacement create request because the first response was inconclusive, and do not
call `run.cancel` because a wait expired. Both turn an unknown local state into a real change to
the assessment record.

Related concepts: [Guides](/guides), [Examples](/examples), [HTTP API reference](/http-api), and
[Reliability and security](/reliability-security).
