Low-level client (TypeScript)
createRunstateClient, a thin promise-per-endpoint client for infrastructure authors, with no lease renewal or waiting helpers. TypeScript only.
Most code should use the Runstate client. It renews leases, waits for capacity, maps errors to typed classes and resolves names to ids. createRunstateClient does none of that: each method is one HTTP request. It exists for tools built directly on the API, where you want to control every request, including its Idempotency-Key.
import { createRunstateClient, KernelError } from 'runstate-sdk';
const client = createRunstateClient({ baseUrl: process.env.RUNSTATE_BASE_URL ?? 'https://api.getrunstate.com', apiKey: process.env.RUNSTATE_API_KEY!,});const space = client.space(process.env.RUNSTATE_SPACE_ID!);
const run = await space.createScope({ idempotencyKey: `nightly-${new Date().toISOString().slice(0, 10)}` });const mailbox = await space.createMailbox({ name: 'crawl', idempotencyKey: 'create-crawl-queue' });await space.mailbox(mailbox.id).send({ scopeId: run.id, payload: { url: 'https://example.com' }, idempotencyKey: 'crawl-example.com',});
const msg = await space.mailbox(mailbox.id).recv<{ url: string }>({ scopeId: run.id, holder: 'crawler-1', session: 'crawler-1-boot-42', leaseSeconds: 30,});if (msg !== null) { try { await msg.renew(60); // nothing renews for you await msg.complete(); } catch (err) { if (err instanceof KernelError) console.error(err.code, err.httpStatus); throw err; }}Differences from Runstate
Section titled “Differences from Runstate”- You pass identity explicitly.
holderandsessiongo on every claim, receive and acquire. Use a freshsessionper process. - Ids, not names. Methods take mailbox, permit and barrier ids.
- No renewal, no waiting. Leases expire unless you call
renew(). Busy and exhausted responses are thrown immediately. - Idempotency keys are yours. Pass
idempotencyKeyon the create and send methods that accept it. The client doesn’t generate one, and the API rejects creates and sends without one (VALIDATION_FAILED). - Errors are
KernelError(withcodeandhttpStatus), notRunstateErrorsubclasses. Codes the client doesn’t recognize becomeUNAVAILABLE. - Retries. Transport failures are retried up to 3 times with 25–100 ms jitter; error responses are not retried.
- Smaller surface. It covers runs, claims, mailboxes and messages, events, timers, permits and grants, and barriers. Tasks, task groups, quotas, work limits and budgets are only in
Runstateand the HTTP API.
Methods
Section titled “Methods”function createRunstateClient(options: { baseUrl: string; apiKey: string; fetchImpl?: typeof fetch }): RunstateClient;
interface RunstateClient { space(spaceId: string): SpaceClient;}
interface SpaceClient { createScope(opts: { parentId?: string; deadline?: string; childLimit?: number; idempotencyKey?: string }): Promise<{ id: string; state: string; version: string }>; scope(scopeId: string): { get(): Promise<{ id: string; storedState: string; effective: string }>; cancel(): Promise<{ id: string; state: string; version: string }>; complete(): Promise<{ id: string; state: string; version: string }>; };
claim(key: string, opts: { scopeId: string; holder: string; session: string; leaseSeconds?: number; idempotencyKey?: string }): Promise<ClaimHandle>;
createMailbox(opts: { name: string; mode?: 'WORK' | 'INBOX'; backlogLimit?: number; idempotencyKey?: string }): Promise<{ id: string; name: string; mode: string }>; mailbox(mailboxId: string): { send(opts: { scopeId: string; payload: unknown; workKey?: string; deadline?: string; idempotencyKey?: string }): Promise<{ id: string; replay: boolean }>; recv<T = unknown>(opts: { scopeId: string; holder: string; session: string; leaseSeconds?: number }): Promise<MessageHandle<T> | null>; };
events(opts?: { cursor?: string; limit?: number }): Promise<EventPage>;
armTimer(opts: { scopeId: string; at?: string; afterSeconds?: number; idempotencyKey?: string }): Promise<{ id: string; fireAt: string }>; pollTimers(scopeId?: string): Promise<FiredTimer[]>; ackTimer(timerId: string, token: string): Promise<{ state: string }>; cancelTimer(timerId: string): Promise<{ state: string }>;
createPermit(opts: { name: string; units: number; idempotencyKey?: string }): Promise<{ id: string; name: string; unitsTotal: number }>; acquirePermit(permitId: string, opts: { scopeId: string; units: number; holder: string; session: string; leaseSeconds?: number; waiterId?: string }): Promise<GrantAcquired>; waitPermit(permitId: string, opts: { scopeId: string; units: number; holder: string }): Promise<{ waiterId: string }>; releaseGrant(grantId: string, token: string, observation?: unknown): Promise<{ state: string }>;
createBarrier(opts: { scopeId: string; target: number; idempotencyKey?: string }): Promise<{ id: string }>; arriveBarrier(barrierId: string, arrivalKey: string): Promise<{ state: string; arrivals: number }>; barrier(barrierId: string): Promise<BarrierView>;}
interface ClaimHandle { key: string; generation: string; leaseExpiresAt: string; token: string; renew(leaseSeconds?: number): Promise<{ leaseExpiresAt: string }>; release(observation?: unknown): Promise<{ state: string }>;}
interface MessageHandle<T = unknown> { message: { id: string; scopeId: string; payload: T; attempt: number; deadline: string | null }; token: string; generation: string; leaseExpiresAt: string; renew(leaseSeconds?: number): Promise<{ leaseExpiresAt: string }>; nack(retry: boolean): Promise<{ state: string }>; complete(result?: { mailboxId: string; scopeId: string; payload: unknown; workKey?: string }): Promise<{ state: string; resultMessageId?: string }>;}
interface GrantAcquired { grantId: string; token: string; leaseExpiresAt: string }interface FiredTimer { id: string; token: string; firedAt: string; scopeId: string }interface BarrierView { id: string; state: string; epoch: string; arrivals: number; target: number }The low-level complete() type requires mailboxId and scopeId when you pass a result. To record a task outcome without publishing a result message, use delivery.complete({ payload }) from the Runstate client or the HTTP API.