# Nightjar SDK

Submit connected Codex or Claude work from a Node.js backend and collect durable results.

**Node.js 22+ · TypeScript · ESM + CommonJS · No runtime dependencies**

Read the [pilot limits and reliability contract](./pilot.md) before delegating sensitive or irreplaceable work.

## Install and configure

```sh
npm install @silicon-intern/nightjar
```

In the Nightjar dashboard, connect your own Codex or Claude account and create a workspace key under **Developers → API keys**. Account sign-in and tool authorization happen in the dashboard. Your backend only needs the key:

```sh
NIGHTJAR_API_KEY=your_workspace_key
```

```ts
import Nightjar from '@silicon-intern/nightjar';

const nightjar = new Nightjar();
const result = await nightjar.run('Return a short checklist for testing a signup flow.');
console.log(result.output);
```

No saved agent or workspace ID is required. The key identifies its workspace; Nightjar selects a ready model account belonging to the key creator. For self-hosting, also set `NIGHTJAR_BASE_URL`.

Keys stay on the server. Other workspace members' subscriptions are not inherited. Dashboard keys have `runs:read` and `runs:write`; they cannot administer workspaces or connect accounts.

Named imports and CommonJS also work: `import { Nightjar } from '@silicon-intern/nightjar'` or `const { Nightjar } = require('@silicon-intern/nightjar')`.

## Give work access to your tools

With only `NIGHTJAR_API_KEY` configured, the first example grants no Nightjar tool connections. To investigate real repository activity, connect GitHub in the dashboard, then select it in **Developers → Quickstart** and copy the example with its actual connection ID:

```ts
const result = await nightjar.run({
  prompt:
    'Summarize the last seven days of changes in the connected repository. Do not modify anything.',
  connectionIds: ['YOUR_GITHUB_CONNECTION_ID'],
});
console.log(result.output);
```

Replace the placeholder with the selected connection ID. Add other authorized tools through `connectionIds` as needed. Without a saved agent, omitting this list grants no Nightjar service connections; adding a connection to the workspace does not automatically grant it to your runs. The server enforces each connection's saved resource and access limits.

## Submit without blocking your application

```ts
const run = await nightjar.runs.create(
  {
    input: 'Return a short checklist for testing a signup flow.',
    metadata: { jobId: 'signup-checklist-001' },
  },
  { idempotencyKey: 'signup-checklist-001' },
);
// Persist run.id beside your job ID, then return HTTP 202.
// A worker can wait later; a webhook can notify your application instead.
const result = await nightjar.runs.result(run.id);
console.log(result.output);
```

`runs.create` returns after durable acceptance, usually `queued`. It accepts an object; the string shorthand belongs to `run`. Use the **same body and idempotency key** to recover an uncertain submission. A different body with the same key returns `409 idempotency_conflict`; a new key creates new work. Keys contain 1–200 printable ASCII characters without spaces.

`run` combines submission and waiting for success. Both `run` and `runs.result` throw `NightjarRunError` for unsuccessful stopped work, with the snapshot in `error.run`. This includes `failed`, `cancelled`, `needs_input`, and a completed model whose deliverables failed. `runs.wait` returns stopped work for you to handle, while continuing to wait for a completed model's pending publication.

| Status              | Handling                                                                                                  |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `queued`, `running` | Keep waiting or return to the run later.                                                                  |
| `completed`         | Read the result when `lifecycle.resultReady` is true; publication may still be pending or require action. |
| `failed`            | Inspect `error`; partial output is not success.                                                           |
| `cancelled`         | Work was cancelled; keep observing if execution is still unconfirmed.                                     |
| `needs_input`       | Inspect `attention` and `lifecycle`; enable an answer only when `canAnswer` is true.                      |

Waits default to one-second polling and 30 minutes. `timeoutMs` limits local waiting; `requestTimeoutMs` limits each HTTP operation. Run input `timeoutSeconds` controls remote execution (30–86,400 seconds; default 1,800 unless inherited from an agent). Requests and waits accept an `AbortSignal`. A local timeout or abort does not cancel remote work; use `runs.cancel(id)` explicitly.

## Questions, answers, and recovery

Use SDK **1.3.0 or later** with the updated server for `runs.answer`, `runs.recover`, and the additional webhook event types below. SDK 1.2.0 does not contain these methods. Existing integrations can upgrade the SDK or call the HTTP endpoints directly.

Every updated server snapshot includes `lifecycle`: use `resultReady` to recognize a usable completed result, `observing` to keep observing recovery or publication, and `canAnswer` to enable an answer form. `status` remains compatible with existing clients. `needs_input` can mean a question, a connection problem, or unresolved execution; inspect `attention.kind` and `lifecycle` instead of guessing from output. A client connecting to an older server can fall back to `status` when `lifecycle` is absent.

```ts
const run = await nightjar.runs.get(savedRunId);
if (run.lifecycle?.canAnswer && run.attention?.kind === 'question') {
  // Send attention.id, message and optional choices to your application's UI.
  // Obtain the user's answer there; keep the Nightjar key in this backend.
  const continuation = await nightjar.runs.answer(run.id, {
    requestId: run.attention.id,
    answer: answerFromYourUser,
  });
  // Persist and observe continuation.id; conversationId stays stable.
}
```

Both providers receive the internal `request_input` tool, including runs with no service connections. Nightjar records the question before the agent stops; `canAnswer` becomes true after execution is confirmed stopped. An answer is atomic and idempotent by question ID: the same answer returns the same continuation, and a conflicting answer returns `409 answer_conflict`. A cancelled/resolved question rejects later answers. Answering requires write permission and access to the original Environment. HTTP: `POST /api/tasks/:id/answer` with `{requestId, answer}`.

Original goals, accepted answers, and working files persist across continuations. Immediate parent results retain up to 100,000 characters; three earlier turns retain bounded excerpts (4,000 characters of instruction and 16,000 of result each). Accepted answers are limited to 100,000 serialized characters per conversation. Run IDs identify attempts; `conversationId` identifies their shared conversation. Ordinary follow-ups and explicit retries retain lineage.

`lifecycle.state: "recovering"` means Nightjar is observing the existing process. `recovery` reports attempt count, next attempt time, and the latest diagnostic. After five automatic attempts, offer `runs.recover(id)` / `POST /api/tasks/:id/recover` to request another bounded recovery cycle. Recovery never repeats the goal or extends a lease. Keep the reservation until `executionUnconfirmed` clears. A confirmed, stopped run can then be continued with an explicit follow-up.

`runs.wait` returns stopped failures or questions for handling; a completed model with pending publication continues waiting. `runs.result` rejects a completed run whose deliverables failed. The activity iterator remains open while `lifecycle.observing` is true, including post-execution publication/recovery updates.

Opt in to server-delivered Slack notifications with `notification: {connectionId: selectedSlackId}` when creating the run. The selected connection must be a ready Slack account with write permission and a saved self destination. This does not grant Slack tools to the agent. Scheduled runs retain the same opt-in; set `notification: null` when editing a schedule to turn it off. Answer continuations remain part of the scheduled occurrence and prevent overlapping work. Notifications survive model failure; `notification.status` reports pending, sent, or failed delivery. An ambiguous send is not blindly repeated: the dashboard asks the user to check Slack before retrying.

Subscribe to `task.attention_requested`, `task.answer_received`, `task.recovering`, `task.recovered`, and `task.updated` to observe questions, answers, recovery and publication/notification changes. All contain immutable full task snapshots with the same signed envelope and sequence rules. `task.attention_requested` can arrive before the pause is complete; the later `task.needs_input` contains `canAnswer: true` when it is safe. Existing subscriptions retain their four default stopped events until explicitly changed.

The runnable repository example, `scripts/examples/conversation-backend.ts`, demonstrates a signed receiver with durable duplicate/out-of-order handling and an answer endpoint. Run it with Node.js 24 and `pnpm exec tsx scripts/examples/conversation-backend.ts`, providing `NIGHTJAR_API_KEY`, `NIGHTJAR_WEBHOOK_SECRET`, and a separate `EXAMPLE_APP_TOKEN`. Its authenticated demo endpoints listen on loopback port 8787; send `Authorization: Bearer EXAMPLE_APP_TOKEN` to create, read, or answer a job. The receiver stores full snapshots and acknowledges compact progress without applying it. Reads reconcile with Nightjar. Expose the webhook route through your own public HTTPS endpoint for real delivery, and replace the demo authentication with your application's session and ownership rules before exposing job endpoints to end users.

## Structured results

Supply a provider-compatible JSON schema and a runtime parser for output your code depends on:

```ts
const result = await nightjar.run(
  {
    prompt: 'Summarize the most important checks for testing a signup flow.',
    outputSchema: {
      type: 'object',
      properties: { summary: { type: 'string' } },
      required: ['summary'],
      additionalProperties: false,
    },
  },
  {
    parse(value: unknown): { summary: string } {
      if (
        typeof value !== 'object' ||
        value === null ||
        !('summary' in value) ||
        typeof value.summary !== 'string'
      )
        throw new Error('Expected a string summary.');
      return { summary: value.summary };
    },
  },
);
console.log(result.structuredOutput.summary);
```

An existing validator works through `parse: (value) => schema.parse(value)`; async parsers also work. Parsing applies to `structuredOutput` and is available on `runs.result`. Without a parser, structured output remains `unknown`. Parser failure throws `NightjarOutputError`, with `run` and `cause`, without rerunning the model.

Nightjar sends the schema unchanged to the provider and validates the result. Schemas must be under 16 KB without references, patterns, or formats. For portability, use a Draft-07 object schema with required properties and `additionalProperties: false` on every object. Provider compatibility can still differ; unsupported schemas fail instead of being silently rewritten.

## Live applications

In an enabled pilot workspace, describe the application and ask for a public URL. Static and React/Vite publishing tools are supplied automatically to the native agent; no Vercel connection is needed. The agent builds in its Environment, reads structured errors, and can repair and retry within the same run. Updates reuse the app's stable Vercel URL.

```ts
const result = await nightjar.run({
  prompt: 'Build a React habit tracker and give me a public URL.',
  connectionIds: [],
});
for (const app of result.apps ?? []) {
  if (app.state === 'live' && app.liveUrl) console.log(app.liveUrl);
  else console.log(app.state, app.error);
}
```

`apps` is server-generated metadata, separate from `output` and any requested `structuredOutput` schema. SDK 1.2.0 includes these types; install `@silicon-intern/nightjar@^1.2.0` from npm. A completed run alone does not prove successful deployment. If an app is pending, reread the run with `runs.get(id)` until its deployment becomes terminal. App changes after the model stops emit `task.updated` with the full task snapshot; subscribe to it to observe publication through to readiness.

Published files are public. Servers, SSR, databases, and app secrets are unsupported. The current pilot allows ten apps per workspace, subject to the ten-app global pilot allowance, and 25 MiB per deployment, with a finite hosting lease. See the [pilot limits and current lease](./pilot.md#hosting-lease) before depending on a public URL. Publication metadata records the last verified deployment; a later operator pause or lease expiry can make that URL unavailable.

## Project downloads and GitHub work

The run's **Outputs** panel offers **Download ZIP** for captured project files, plus **View branch** and **View PR** when work was delivered to GitHub. **Download summary** saves the Markdown response; structured results keep their **Download JSON** action.

Run snapshots add optional `artifacts`, `github`, and `artifactError` fields. The existing `output` summary and `structuredOutput` values keep the same shape. Older runs and runs without files may have no artifacts. GitHub links identify verified remote branches and pull requests; they are not inferred from links in the model's summary. To request a push or pull request, select a connection with write access and include that instruction in the goal.

After a cloud run stops and its process cleanup is confirmed, Nightjar captures the projects it changed, including each project's unchanged files. Failed or cancelled runs can therefore have partial project files. Unrelated projects, Git history, dependencies, caches, common credential files, non-template `.env` files, and symlinks are excluded. Capture is limited to 10,000 eligible entries, 100 MiB before compression, and a 25 MiB ZIP. The pilot has a 250 MiB total artifact storage limit. No archive is created when there are no eligible project changes; a capture or storage failure appears as `artifactError` without replacing the run result.

`runs.downloadArtifact` was added in SDK **1.1.0** and is included in the current npm release:

```sh
npm install '@silicon-intern/nightjar@^1.2.0'
```

```ts
import { writeFile } from 'node:fs/promises';

const result = await nightjar.runs.result(run.id);
const project = result.artifacts?.[0];
if (project) {
  const bytes = await nightjar.runs.downloadArtifact(result.id, project.id, {
    requestTimeoutMs: 30_000,
    maxRetries: 1,
  });
  await writeFile('./project.zip', bytes);
}
for (const delivery of result.github ?? []) {
  console.log(delivery.repository, delivery.branchUrl);
  if (delivery.pullRequest) console.log(delivery.pullRequest.url);
}
if (result.artifactError) console.error(result.artifactError);
```

Each artifact includes `id`, `name`, `mediaType: 'application/zip'`, `sizeBytes`, `sha256`, and an authenticated `downloadUrl`. The SDK builds the request from the run and artifact IDs against its configured Nightjar server; it does not follow the metadata URL or redirects. It returns a `Uint8Array`, limits the download to 25 MiB, and supports the same authentication, request deadline, `AbortSignal`, and opt-in GET retries as other reads. Stopping a download does not cancel the run.

For HTTP integrations, use `GET /api/tasks/{runId}/artifacts/{artifactId}` with your workspace key. The route requires the same run visibility and `runs:read` scope as reading the task. A successful response is `application/zip`; errors use the usual JSON error envelope. The `downloadUrl` does not grant public access. Saved ZIPs are served by Nightjar and remain downloadable while the Environment sleeps. Check `artifactError` when capture is unavailable.

GitHub delivery metadata includes `repository`, `branch`, `branchUrl`, `commitSha`, and an optional `pullRequest` with `number`, `title`, and `url`. Signed task webhooks carry the same optional metadata, so a backend can download files after processing a completion event. Existing SDK 1.0 methods and result parsing are unchanged.

## Receive signed webhooks

Use signed webhooks to update your application as runs start, make progress, ask a question, recover, and deliver results. A successful submission returns the run ID; your backend receives subsequent updates without polling. Relay verified updates from your backend to your own app clients using your application's realtime transport. Nightjar does not expose a resumable SSE stream.

Register a public HTTPS receiver in **Developers → Webhooks**, choose its events, and save its one-time signing secret on your backend. Events cover the workspace; use `runId` and `data.task.metadata` to correlate your jobs. Existing endpoints and new endpoints that omit event selection receive only the four stopped event types. Select the additional events in the contract below to receive questions, answers, recovery, and result-readiness changes. Progress is optional when your application only needs state and results.

Upgrade the receiver before expanding subscriptions. SDK **1.2.0** supports queued, started, progress, and stopped events; SDK 1.1.0 and earlier typed parsers accept stopped events only:

```sh
npm install '@silicon-intern/nightjar@^1.2.0'
```

The five conversation/update event types below require SDK **1.3.0 or later**. Verify the installed package supports them before selecting them; the 1.2.0 parser rejects those event types.

Verify the exact raw bytes before JSON parsing. This example uses an application-provided durable inbox:

```ts
import express from 'express';
import { parseWebhook, NightjarWebhookError } from '@silicon-intern/nightjar/webhooks';

const app = express();
app.post('/webhooks/nightjar', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const event = parseWebhook({
      payload: req.body,
      signature: req.get('nightjar-signature') ?? '',
      secret: process.env.NIGHTJAR_WEBHOOK_SECRET!,
    });
    // Implement this in your application: atomically save the payload with a
    // UNIQUE constraint on event.id. Already-saved IDs are a successful no-op.
    await saveToYourDurableInbox(event.id, event);
    res.sendStatus(204);
  } catch (error) {
    if (error instanceof NightjarWebhookError) {
      res.sendStatus(error.code === 'invalid_signature' ? 401 : 400);
    } else {
      res.sendStatus(500); // A failed durable save must trigger redelivery.
    }
  }
});
app.use(express.json());
```

`saveToYourDurableInbox` is your persistence function, not an SDK method. Acknowledge with HTTP `2xx` only after durable acceptance, then process the saved event asynchronously. An event can arrive before your submission handler finishes saving the returned run ID; sending your job ID in run `metadata` helps correlate that race. Keep signing secrets, workspace keys, and payload verification on the server.

For receivers without the SDK, the `nightjar-signature` header has the form `t=UNIX_SECONDS,v1=LOWERCASE_HEX_DIGEST`. Compute HMAC-SHA256 with the signing secret over the UTF-8 timestamp, a literal period, then the exact raw request body bytes. Compare the digest in constant time and reject malformed headers and timestamps outside your allowed clock skew. The SDK's default tolerance is five minutes; keep clocks synchronized. Verify before parsing, and never reconstruct the bytes from parsed JSON. `verifyWebhook` performs this signature check and returns a boolean without validating the event shape; `parseWebhook` verifies and validates the event envelope.

## Webhook event contract

New task events have `{ id, type, createdAt, runId, workspaceId, sequence, data }`. IDs are stable strings beginning with `evt_`; `runId` equals `data.task.id`, and `workspaceId` equals `data.task.workspaceId`. `sequence` is a positive, durable activity ID assigned in increasing order by the single-host database. Compare it **within each run** to order events; gaps are normal because other activity and filtered events use the same ID space.

| Event                      | When it is emitted                                                                                 | Payload                                                                              |
| -------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `task.queued`              | The run is durably accepted. An idempotent submission replay does not create another queued event. | `data.task`: full queued run snapshot.                                               |
| `task.started`             | The worker claims the run and begins dispatching it to the runner.                                 | `data.task`: full running run snapshot.                                              |
| `task.progress`            | A meaningful activity update is persisted while the run is queued or running.                      | Compact `data.task` identity/status/metadata and `data.progress`.                    |
| `task.completed`           | The provider completes successfully; publication may still be pending.                             | Full `data.task`; check `lifecycle.resultReady` before treating the result as ready. |
| `task.failed`              | The run stops with an error.                                                                       | `data.task`: full failed run snapshot.                                               |
| `task.cancelled`           | Cancellation is recorded.                                                                          | Full `data.task`; `executionUnconfirmed` can still require recovery.                 |
| `task.needs_input`         | The provider stops for a question or a problem.                                                    | Full `data.task`; inspect `attention` and `lifecycle.canAnswer`.                     |
| `task.attention_requested` | A question is saved, before execution necessarily finishes pausing.                                | Full `data.task` with the question ID, message, and optional choices.                |
| `task.answer_received`     | An accepted answer is linked to its continuation.                                                  | Full parent `data.task`; observe `attention.continuationId` next.                    |
| `task.recovering`          | Recovery is requested or its attempt/diagnostic changes.                                           | Full `data.task` with `recovery` and `executionUnconfirmed`.                         |
| `task.recovered`           | The prior process is confirmed stopped and its Environment reservation is released.                | Full `data.task`; a pending question may now be answerable.                          |
| `task.updated`             | Publication, notification delivery, or the queue explanation changes.                              | Full `data.task`; re-evaluate `lifecycle`, including result readiness.               |

Progress is a human-readable update, not a token stream or a percentage-complete estimate. It includes agent messages, tool activity, execution notices, and relevant errors; it excludes reasoning, raw token deltas, and result payloads. `data.progress.type` preserves the source activity type: `assistant`, `tool`, `system`, `error`, `agent.message`, `tool.called`, `tool.failed`, `git.transfer`, `runner.accepted`, `runner.unreachable`, or `task.cancel_requested`. `data.progress.message` is bounded to 16,000 characters. An idle or silent provider may produce no progress messages. The compact task contains only `id`, `workspaceId`, `status` (`queued` or `running`), and `metadata`; completion events retain the full result.

Illustrative progress payload, not evidence of an executed run:

```json
{
  "id": "evt_example",
  "type": "task.progress",
  "createdAt": "2026-09-08T12:00:03.000Z",
  "runId": "task_example",
  "workspaceId": "ws_example",
  "sequence": 103,
  "data": {
    "task": {
      "id": "task_example",
      "workspaceId": "ws_example",
      "status": "running",
      "metadata": { "jobId": "signup-checklist-001" }
    },
    "progress": {
      "type": "agent.message",
      "message": "I am checking the signup flow and its validation rules."
    }
  }
}
```

The dashboard's **Send test event** emits `webhook.test`, which has `{ id, type, createdAt, data: { workspaceId, message } }` and no task or sequence. It bypasses event filtering, requires an enabled endpoint, and does not run an agent. `parseWebhook` returns a discriminated union covering test, progress, lifecycle, and terminal events. Previously queued terminal payloads may lack the new envelope fields; SDK 1.2.0 continues accepting them. For those payloads, use `data.task.id` and `data.task.workspaceId`; do not invent a missing sequence.

## Webhook delivery and ordering

Events and their subscribed deliveries are saved durably with their originating activity. Delivery runs independently of agent execution, so a long run does not hold its own queued or progress notifications until completion. The pilot dispatches due deliveries in batches of up to eight on an independent timer; this is not a latency SLA.

Delivery is **at least once**, with up to eight automatic attempts in total. Failed requests retry with exponential delays starting at five seconds (5, 10, 20, 40, 80, 160, and 320 seconds between attempts), subject to dispatch timing. Retries preserve the event ID and exact JSON body; each attempt receives a fresh timestamp and signature. Headers also include `nightjar-event-id`, `nightjar-delivery-id`, and `nightjar-delivery-attempt`. The attempt header is diagnostic; authenticate and deduplicate using the signed body. Deduplicate using the verified body's `event.id`, not its signature or delivery ID.

Arrival order is not guaranteed: independent requests, receiver failures, and retries can cause a newer event to arrive first. In your inbox worker, store the highest applied `sequence` per run and atomically apply newer snapshots only. Keep older events in an ordered activity history if useful, but do not let an older queued/progress event overwrite a completed status. A compact progress payload should update the latest message and status; it must not erase full run fields such as output or artifacts. Track each run separately, and do not wait for contiguous sequence numbers. Handle terminal payloads without sequence separately during an upgrade, without allowing them to regress a newer terminal state.

Inspect attempts in **Developers → Webhooks → Recent deliveries**. After automatic attempts are exhausted, **Retry delivery** sends the same event again; it does not rerun the agent. A process interruption can consume a reserved attempt. Manual retry is rejected while an attempt is in flight; wait for it to finish before retrying. An unavailable receiver or exhausted retries can prevent live updates from arriving. Preserve run IDs so you can reconcile with `runs.get` when needed. This recovery read is optional error handling; receiving queued, started, progress, and terminal updates does not require continuous polling.

## Webhook subscriptions

Use **Developers → Webhooks → Add endpoint** to choose events during creation, or **Edit events** on an existing endpoint. A workspace administrator manages these subscriptions in a signed-in browser session. Scoped workspace run keys cannot create, update, test, or retry webhooks.

The underlying administration API accepts the same nonempty list of supported task event names. The following JSON is an illustrative request body for `POST /api/webhooks`, authenticated with an administrator browser session:

```json
{
  "workspaceId": "YOUR_WORKSPACE_ID",
  "url": "https://your-app.example/webhooks/nightjar",
  "eventTypes": [
    "task.queued",
    "task.started",
    "task.progress",
    "task.attention_requested",
    "task.answer_received",
    "task.recovering",
    "task.recovered",
    "task.updated",
    "task.completed",
    "task.failed",
    "task.cancelled",
    "task.needs_input"
  ]
}
```

Creation returns `{ webhook, secret }`; save the one-time `secret` on your receiving server. Omitting `eventTypes` preserves the default selection: `task.completed`, `task.failed`, `task.cancelled`, and `task.needs_input`. Existing endpoints without this field use those same defaults.

To replace the selection, send `PATCH /api/webhooks/{webhookId}` in an administrator browser session:

```json
{
  "eventTypes": ["task.started", "task.progress", "task.completed", "task.failed"]
}
```

Omitting `eventTypes` in a patch leaves the selection unchanged; an empty list or unknown event name is rejected. Set `enabled: false` or use **Pause** to stop an endpoint. Subscriptions affect **future events only**: opting in does not backfill earlier activity, and changing the selection does not remove already queued deliveries. Test events bypass this selection.

## Progress and control

```ts
for await (const event of nightjar.runs.events(run.id, { after: 0 })) {
  console.log(event.id, event.type, event.message);
  // Save event.id after processing; pass it as "after" to resume.
}
```

This SDK iterator polls persisted activity and stops when the run no longer needs observation; it is not SSE. The numeric cursor is filtered locally and does not reduce server response size. These numeric activity IDs supply `sequence` on new webhooks, but differ from the stable string webhook `id`. Use signed lifecycle webhooks above when your app needs updates without polling.

- `runs.followUp(id, { prompt }, { idempotencyKey })` creates a run with prior context when `lifecycle.canFollowUp` is true.
- `runs.answer(id, { requestId, answer }, options?)` answers the exact saved question when `lifecycle.canAnswer` is true and returns its continuation.
- `runs.recover(id)` requests another bounded recovery cycle after automatic attempts are exhausted; it does not repeat model work.
- `runs.retry(id, { idempotencyKey })` creates new model work. Review any previous external changes first.
- `runs.cancel(id)` requests cancellation. Poll until final; stopping external processes can take time.

HTTP retries are off by default. Set `maxRetries` to retry GET requests or create/follow-up/answer/retry requests with an explicit idempotency key. Network errors and HTTP `408`, `429`, `500`, `502`, `503`, and `504` are retryable, within the request deadline. Cancellation, recovery, and unkeyed writes are never automatically retried. Repeating an answer manually with the same question ID and answer is safe even without an idempotency key. An HTTP retry recovers a request; `runs.retry` starts another model run.

## Optional defaults and reuse

Set `provider`, `model`, or `reasoning` when your application needs a specific model configuration. Usually omit `runnerId` so Nightjar selects your account's ready Environment.

```ts
const result = await nightjar.run({
  prompt: 'Return a short checklist for testing a signup flow.',
  provider: 'claude',
  model: 'default',
  timeoutSeconds: 300,
});
```

Choose concrete model IDs and reasoning levels from the dashboard. `default` lets the native provider choose. Discovery reads installed CLI metadata without submitting a prompt; reported models still depend on the account's entitlement and quota. Some model choices may consume provider credits. There is no SDK `models.list` method.

Saved agents are optional reusable instructions and defaults. Existing `agentId` integrations continue to work. Discover them with `nightjar.agents.list()`, then use one for repeated work:

```ts
const reviewer = nightjar.with({ agentId: 'YOUR_SAVED_AGENT_ID' });
const result = await reviewer.run('Review the most recent changes.');
```

Replace the placeholder with an agent ID. The run inherits its instructions, provider, model, reasoning, connections, output schema, and deadline, and records the agent revision. Per-run settings override these defaults. `connectionIds` replaces the inherited list; `[]` grants no Nightjar service connections. `outputSchema: null` selects plain text; omission inherits the agent schema. Use `nightjar.with({ agentId: null })` to clear a saved-agent default for subsequent submissions.

| Client option            | Default                                                                           |
| ------------------------ | --------------------------------------------------------------------------------- |
| `apiKey`                 | `NIGHTJAR_API_KEY`; required.                                                     |
| `workspaceId`, `agentId` | Optional corresponding `NIGHTJAR_WORKSPACE_ID` and `NIGHTJAR_AGENT_ID` variables. |
| `baseUrl`                | `NIGHTJAR_BASE_URL`, then `NIGHTJAR_URL`, then the hosted origin.                 |
| `requestTimeoutMs`       | `30_000`, including HTTP retries and backoff.                                     |
| `maxRetries`             | `0`; at most `5` additional attempts.                                             |
| `fetch`                  | `globalThis.fetch`; injected transports must honor `AbortSignal`.                 |

Explicit options override environment variables. `nightjar.with({ workspaceId, agentId })` creates an independent client; omitted values retain defaults, and `null` clears them. Per-run values override client defaults. An explicit `workspaceId` must still match the key's workspace access. Neither optional ID is needed for the first-run examples above.

## API reference

| Method                                               | Result                                                                     |
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
| `run(stringOrInput, options?)`                       | Submit and wait for successful completion.                                 |
| `with({ workspaceId?, agentId? })`                   | Independent client defaults.                                               |
| `runs.create(input, options?)`                       | Accepted task.                                                             |
| `runs.get(id, options?)`                             | Current snapshot.                                                          |
| `runs.downloadArtifact(id, artifactId, options?)`    | Project ZIP as `Uint8Array`; SDK 1.1.0+.                                   |
| `runs.list(workspace?, options?)`                    | At most 500 recent runs; no pagination.                                    |
| `runs.result(id, options?)`                          | Wait for success; optional output parser.                                  |
| `runs.wait(id, options?)`                            | Return stopped work; wait through a completed model's pending publication. |
| `runs.events(id, options?)`                          | Async iterator over persisted events.                                      |
| `runs.cancel(id, options?)`                          | Cancellation request and current snapshot.                                 |
| `runs.followUp(id, { prompt, runnerId? }, options?)` | New run with previous context.                                             |
| `runs.answer(id, { requestId, answer }, options?)`   | One linked continuation; SDK 1.3.0+.                                       |
| `runs.recover(id, options?)`                         | Bounded environment recovery; SDK 1.3.0+.                                  |
| `runs.retry(id, options?)`                           | New run; optional `runnerId`.                                              |
| `agents.list(workspace?, options?)`                  | Saved agents.                                                              |
| `parseWebhook(options)`                              | Verified event; conversation/update types require the SDK 1.3.0+.          |
| `verifyWebhook(options)`                             | Boolean signature check.                                                   |

List methods accept a workspace string or `{ workspaceId }`, or use the client default. HTTP options are `signal`, `requestTimeoutMs`, and `maxRetries`. Submissions additionally accept `idempotencyKey`. Wait/result/event options add `intervalMs` and `timeoutMs`; results accept `parse`, and events accept `after`. `run` supports both result and submission options.

`TaskInput` supports `agentId`, `workspaceId`, `prompt`, `input`, `provider`, `model`, `reasoning`, `connectionIds`, `metadata`, `outputSchema`, `timeoutSeconds`, `runnerId`, `parentTaskId`, and `notification`. Prefer `runs.followUp` for prior context. Import public types from the package root; `Run` aliases `Task`, and `CompletedRun<T>` narrows successful structured results.

## Errors

| Error                     | Handling                                                                                     |
| ------------------------- | -------------------------------------------------------------------------------------------- |
| `NightjarError`           | Inspect HTTP `status`, optional `code`, `requestId`, and `retryAfterMs`.                     |
| `NightjarConnectionError` | Check network/base URL; inspect `cause`.                                                     |
| `NightjarProtocolError`   | Unexpected response shape; check SDK/server compatibility.                                   |
| `NightjarRunError`        | Inspect `run.lifecycle`, `run.attention`, `run.error`, and deliverable errors in `run.apps`. |
| `NightjarTimeoutError`    | Resume waiting with `taskId` or cancel explicitly.                                           |
| Request `TimeoutError`    | Recover uncertain submissions using the original body/key.                                   |
| `NightjarWebhookError`    | Check raw bytes, signature, secret, clock, and event shape.                                  |
| `NightjarOutputError`     | The runtime parser rejected the completed result; inspect `run` and `cause`.                 |

For `model_account_required`, connect the key creator's own account. For `connection_not_ready`, recheck or reconnect the selected tool. `idempotency_conflict` requires the original request body or a new key for a new job. `queue_full` means wait for current work to finish. Administrative operations require a signed-in browser session.

For `question_not_current`, `answer_conflict`, or `continuation_exists`, reread the run and use its current question or linked continuation. `execution_unconfirmed` means wait for cleanup or recovery before answering or starting another attempt. `recovery_not_required` means the state changed before your request; reread it instead of retrying recovery.

## Environments and execution limits

An Environment is the execution setup holding native model sign-ins, installed tools, and working files. The SDK can use the ready Environment for your account without a separate setup call.

New runs have native shell and file tools in a persistent directory dedicated to their conversation. Continuations reuse it. Legacy Prized runs retain `~/workspace` and remain exclusive. Workers provide Node.js 24, pnpm, Python 3, Git, build tools, curl, ripgrep, and unzip. Private HTTPS Git clones use the selected connection; pushes require write access. SSH, Git LFS, workflow-file permission, and managed browser/computer use are unsupported.

**The VM is the trust boundary.** Full-permission code can access worker files and model authentication; selected connections cannot constrain credentials independently installed there. Do not share one worker between mutually untrusted users or secrets. Control-plane credentials stay on the server. Cancellation cannot undo writes or guarantee cleanup of deliberately hostile code.

Managed pilot Environments and explicitly enrolled legacy Environments disable idle pause within their original finite lease. The provider idle heuristic is not reliable evidence that an agent has stopped. Recovery can wake the exact existing Environment and restore its earliest hard deadline; it cannot extend it. Files and native sign-ins persist; disk storage remains billable.

Cloud concurrency defaults to one. The operator can set `NIGHTJAR_CLOUD_CONCURRENCY=2` for two independent conversations after measuring the workload. Same-conversation work, legacy shared directories, and local execution remain sequential; uncertain execution reserves the entire Environment. Each isolated cloud run has a 1,280 MiB process RSS guard and conservative build settings. These controls are scheduling safeguards, not security isolation or a hard OS memory reservation.

## Upgrade from 0.2.0

Change imports from `nightjar-sdk` to `@silicon-intern/nightjar`, or preserve them with an alias:

```sh
npm install 'nightjar-sdk@npm:@silicon-intern/nightjar@^1.0.0'
```

Existing named imports, explicit client options, `runs` methods, types, and `verifyWebhook` remain. `runs.wait` still returns unsuccessful stopped runs. Legacy `createTask`, `getTask`, `cancelTask`, and `listTasks` remain; `getTask` retains `{ task, events }`. Environment-variable defaults are new: keep explicit options when ambient variables should not choose your workspace or agent.

The original hosted `nightjar-sdk@0.2.0` archive remains at `/downloads/nightjar-sdk-0.2.0.tgz`, and the scoped 1.0.0 archive remains at `/downloads/silicon-intern-nightjar-1.0.0.tgz`. Both are frozen for existing lockfiles.

## Validate an integration

Submit a harmless goal with a stable job ID, repeat the same body/key to confirm one run, and check its real result. Then submit a connected read with explicit `connectionIds` and confirm the result uses the intended resource. Verify signed task and test webhooks, including duplicate delivery. Handle failures and resume a local timeout by run ID. Test with each provider your application supports. Scheduling is optional.

## Instructions for coding agents

Read this guide and the installed TypeScript declarations before integrating. Use `runs.create` with a stable idempotency key for HTTP handlers, save run IDs, and collect results through workers or verified webhooks. Keep keys out of browser code, prompts, and metadata. Handle unsuccessful runs explicitly and validate structured output at runtime.

Use injected `fetch` transports for local tests and report real cloud validation separately. This guide ships as `node_modules/@silicon-intern/nightjar/GUIDE.md`; its copy matches the installed package, while hosted docs are deployed separately.

## Package maintenance

Maintainers: `pnpm build:sdk` builds ESM/CommonJS and declarations, copies this guide, packs the package, and generates hosted docs. Run `pnpm check` and SDK tests before release. Preserve the legacy archive. An npm publication and a hosted deployment are separate operations.
