Configuration & auth
Environment variables, client options, API key permissions, worker identity, timeouts and transport retries for the TypeScript and Python SDKs.
Environment variables
Section titled “Environment variables”Both SDKs read the same three variables. Explicit constructor options take precedence.
| Variable | Required | Default | Purpose |
|---|---|---|---|
RUNSTATE_API_KEY |
yes | none | API key, sent as Authorization: Bearer <key> on every request. |
RUNSTATE_SPACE_ID |
yes | none | The space (project or environment) every call is bound to. |
RUNSTATE_BASE_URL |
no | http://localhost:8080 |
API origin. Set it to https://api.getrunstate.com for the hosted service. |
If the API key or space id is missing, constructing the client throws ConfigError immediately, before any network call. There is no environment variable for the holder name; pass it as an option.
Creating a client
Section titled “Creating a client”import { Runstate } from 'runstate-sdk';
const rs = new Runstate({ baseURL: 'https://api.getrunstate.com', apiKey: process.env.RUNSTATE_API_KEY, spaceId: process.env.RUNSTATE_SPACE_ID, holder: 'research-worker-1', // stable name for this logical worker requestTimeoutMs: 10_000, // per attempt retries: 2, // transport retries after the first attempt});import os
from runstate import AsyncRunstate
rs = AsyncRunstate( base_url="https://api.getrunstate.com", api_key=os.environ["RUNSTATE_API_KEY"], space_id=os.environ["RUNSTATE_SPACE_ID"], holder="research-worker-1", # stable name for this logical worker request_timeout_ms=10_000, # per attempt retries=2, # transport retries after the first attempt)Python has two clients with the same object graph: AsyncRunstate (async, the primary client) and Runstate (blocking; runs the async client on a dedicated background thread). See the Python SDK reference.
| Option (TS / Python) | Default | Notes |
|---|---|---|
apiKey / api_key |
RUNSTATE_API_KEY |
Required. |
spaceId / space_id |
RUNSTATE_SPACE_ID |
Required. |
baseURL / base_url |
RUNSTATE_BASE_URL, else http://localhost:8080 |
Trailing slashes are removed. |
holder / holder |
random worker-xxxxxx |
Logical worker name, stable across restarts. |
requestTimeoutMs / request_timeout_ms |
10000 |
Deadline for each attempt, not the whole call. |
retries / retries |
2 |
Extra attempts after a transport failure. |
fetchImpl (TS only) |
global fetch |
Test seam. |
Constructing a client makes no network calls. The TypeScript client holds no connections, and rs.close() exists only for symmetry. In Python, close the client when you’re done: await rs.aclose() or async with AsyncRunstate(...) as rs: (blocking client: rs.close() or with Runstate(...) as rs:).
To use several spaces from one process, rs.withSpace(spaceId) (Python: rs.with_space(space_id)) returns a client bound to another space that shares the configuration and identity.
API keys and permissions
Section titled “API keys and permissions”Create and revoke keys in the console under API keys in a space. The secret is shown once and never stored in readable form. Organization, space and key management is console-only today; there is no public API or SDK method for it.
Each key has a set of permissions. Using coordination while agents run is separate from setting it up, so a worker key doesn’t need to be able to create or resize resources:
| Permission | Allows |
|---|---|
coordination_read |
Reading runs, claims, queues and messages, tasks, task groups, events and watch, and the resources agents use: pools, quotas, budgets, barriers, work limits and timers (run.status(), ticket.result(), budget.status(), name lookups). |
coordination_write |
Runtime work: creating, cancelling and completing runs, claims, sending and receiving messages, submitting, admitting, cancelling and joining tasks, task groups, taking and returning pool units, taking quota units and reporting cooldowns, reserving, settling and voiding budget, barrier arrivals, timers. |
resource_config |
Setting up the space: creating queues, pools, quotas, budgets, barriers, work limits and webhook destinations (every ensure() that has to create), and changing space limits (PUT /limits). |
usage_read |
Usage, space limits, diagnostics (rs.diagnostics, census, backlog, waits, run and task views) and webhook delivery history. |
Keys created in the console start with all four. A typical least-privilege split is an admin key with all four that runs the ensure() calls once, and worker keys with coordination_read and coordination_write. With a worker key, ensure() still works for resources that already exist with the same settings: the SDK verifies the existing resource instead of creating it, and throws FORBIDDEN only when the resource is missing. Every operation’s permission is listed in the HTTP API reference.
A key without the permission a call needs gets FORBIDDEN with the message credential lacks <permission>.
A key can also be limited to specific spaces; using it for another space returns FORBIDDEN. An id from another organization returns NOT_FOUND, so resource ids don’t leak between tenants.
Worker identity
Section titled “Worker identity”Every claim, delivery and pool lease records a holder and a session:
holdernames the logical worker. Keep it stable across restarts (for examplecrawler-3or the pod name) so the console and diagnostics show who owns what.- The session is generated fresh every time you construct a client. Two live processes never present the same holder and session pair, even if you give them the same holder name.
Timeouts, retries and idempotency
Section titled “Timeouts, retries and idempotency”- Per-attempt timeout.
requestTimeoutMsapplies to each HTTP attempt. With the defaults (10 s, 2 retries) a call to an unreachable API can take about 30 seconds before it throwsUnavailableError. - Transport retries only. The SDKs retry network failures and timeouts, with 25 to 100 ms of jitter. They never retry a well-formed error response such as
CLAIM_HELDorINSUFFICIENT_BUDGET; those surface as typed errors immediately. Waiting helpers likeclaim.acquire({ wait: true })handle specific codes on top of this, see Errors & retries. - Idempotency keys. Every non-
GETrequest carries a freshIdempotency-Keyheader, reused across the transport retries of that one call, so a retried request is never applied twice. Separate calls get separate keys: callingrs.scopes.create()twice creates two runs. - Client-side waits end locally. Methods like
ticket.result(),group.wait()andtimer.wait()poll until theirtimeoutMsand then throwWaitTimeoutError. The task, group or timer keeps going on the server.