> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://platform.bctrl.ai/llms.txt.
> For full documentation content, see https://platform.bctrl.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://platform.bctrl.ai/_mcp/server.

# SDK Essentials

> Pagination, streaming, errors, idempotency, and schema authoring across the SDK.

Patterns that apply across every resource in the SDK.

## Pagination

List methods return one page: an object with `data` and `nextCursor`. `nextCursor` is `null` on the last page. To walk every page, use `iter`, an async generator that fetches each next page for you.

```ts
// One page
const { data } = await bctrl.runtimes.list({ spaceId: space.id });

// Every page
for await (const runtime of bctrl.runtimes.iter({ spaceId: space.id })) {
  console.log(runtime.id);
}
```

Every resource exposes both `list` and `iter`.

## Streaming

Run events and activity expose a `streamUrl` that returns a server-sent events endpoint. Consume it with any SSE client:

```ts
const url = bctrl.runs.events.streamUrl(runId, { type: "console.message" });

const source = new EventSource(url);
source.onmessage = (message) => {
  const event = JSON.parse(message.data);
  console.log(event.type, event.name);
};
```

## Errors

The SDK throws typed errors. Catch the base class, or narrow to a specific one:

```ts
import {
  BctrlApiError,
  BctrlAuthenticationError,
  BctrlNotFoundError,
  BctrlRateLimitError,
  BctrlValidationError,
} from "@bctrl/sdk";

try {
  await bctrl.runtimes.get("rt_missing");
} catch (error) {
  if (error instanceof BctrlNotFoundError) {
    // 404
  } else if (error instanceof BctrlRateLimitError) {
    // 429 - back off and retry
  } else if (error instanceof BctrlApiError) {
    console.error(error.code, error.requestId);
  }
}
```

The controller-busy case has a dedicated helper:

```ts
if (Bctrl.isControllerBusy(error)) {
  // another controller holds the runtime
}
```

## Idempotency

Billable POSTs - starting a runtime, creating an invocation, exporting run files - accept an idempotency key so a retried request does not run twice:

```ts
await bctrl.runtimes.start(runtime.id, { idempotencyKey: "boot-2024-06-01" });

await bctrl.runtimes.invocations.create(
  runtime.id,
  { action: "act", instruction: "Click submit." },
  { idempotencyKey: "submit-once" }
);
```

A replayed key returns the original result rather than creating a duplicate.

## Schema authoring

[Invocations](/sdk/invocations) that extract structured data accept a Zod schema as `schema`. The SDK converts it to JSON Schema on the wire and validates the output:

```ts
import { z } from "zod";

const invocation = await bctrl.runtimes.invocations.stagehand.extract(runtime.id, {
  instruction: "Extract the invoice.",
  schema: z.object({
    number: z.string(),
    total: z.number(),
  }),
});
```

Pass `schema`, not `outputSchema` - the latter is the wire field the SDK produces for you.

## Next

* [Runs](/sdk/runs) - what streaming subscribes to
* [Invocations](/sdk/invocations) - where schemas are used
* [API Reference](/api) - status codes and error shapes