SDK Essentials

View as Markdown

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.

1// One page
2const { data } = await bctrl.runtimes.list({ spaceId: space.id });
3
4// Every page
5for await (const runtime of bctrl.runtimes.iter({ spaceId: space.id })) {
6 console.log(runtime.id);
7}

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:

1const url = bctrl.runs.events.streamUrl(runId, { type: "console.message" });
2
3const source = new EventSource(url);
4source.onmessage = (message) => {
5 const event = JSON.parse(message.data);
6 console.log(event.type, event.name);
7};

Errors

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

1import {
2 BctrlApiError,
3 BctrlAuthenticationError,
4 BctrlNotFoundError,
5 BctrlRateLimitError,
6 BctrlValidationError,
7} from "@bctrl/sdk";
8
9try {
10 await bctrl.runtimes.get("rt_missing");
11} catch (error) {
12 if (error instanceof BctrlNotFoundError) {
13 // 404
14 } else if (error instanceof BctrlRateLimitError) {
15 // 429 - back off and retry
16 } else if (error instanceof BctrlApiError) {
17 console.error(error.code, error.requestId);
18 }
19}

The controller-busy case has a dedicated helper:

1if (Bctrl.isControllerBusy(error)) {
2 // another controller holds the runtime
3}

Idempotency

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

1await bctrl.runtimes.start(runtime.id, { idempotencyKey: "boot-2024-06-01" });
2
3await bctrl.runtimes.invocations.create(
4 runtime.id,
5 { action: "act", instruction: "Click submit." },
6 { idempotencyKey: "submit-once" }
7);

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

Schema authoring

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:

1import { z } from "zod";
2
3const invocation = await bctrl.runtimes.invocations.stagehand.extract(runtime.id, {
4 instruction: "Extract the invoice.",
5 schema: z.object({
6 number: z.string(),
7 total: z.number(),
8 }),
9});

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

Next