# Concepts

> Spaces, runs, work queues, tasks, deliveries, claims and leases, and how the names in these docs map to the names in the SDKs and API.

Source: https://docs.getrunstate.com/concepts/

runstate is a small set of coordination building blocks. Your agents call them; runstate stores the result durably and applies the same rules to every caller. This page defines the terms the guides use.

## Mechanism, not policy

runstate decides nothing about *what* your agents should do. It records and enforces the things they need to agree on: who owns a key, whether a task has a result, how many units of a quota are left, whether a budget can cover a reservation, whether a run is still accepting work. Retry strategy, prioritisation and what counts as a good result stay in your code.

That split is also why runstate works with any framework: it only needs a few calls at the points where agents would otherwise conflict.

## The building blocks

### Space

A **space** is a project or environment inside your organization, for example `production` and `staging`. Every API path starts with `/v1/spaces/{spaceId}`, and every SDK client is bound to one space (`RUNSTATE_SPACE_ID`). Plan limits are counted across the whole organization, so splitting work into more spaces never buys more capacity.

### Run

A **run** is one execution of your swarm: a research job, an eval batch, a nightly pipeline. Everything agents do inside it (claims, tasks, quota usage, budget reservations) is attached to the run.

Runs form a tree: a run can have child runs. **Cancelling a run refuses new work for it and its whole subtree, immediately, on the server.** Agents that are already working are not killed; their next attempt to take new work fails, and they can still hand back what they hold.

The SDKs and API call a run a **scope**: `rs.scopes.create()` returns a `ScopeHandle`, and API paths use `/scopes`.

### Work queue, task and delivery

A **work queue** is a durable queue that workers pull from. The SDKs and API call it a **mailbox**, created with `mode: 'WORK'` (competing consumers, the default) or `'INBOX'`.

A **task** is a durable unit of work with a stable key and exactly one recorded result. You submit a task to a work queue under a run; runstate delivers it to one worker at a time. The client-side handle is a `TaskTicket`.

A **delivery** is one attempt to hand a task to a worker. If the worker fails or disappears, the task gets a new delivery with `attempt` incremented. The task keeps its identity across deliveries.

Task states: `PENDING` (waiting for a worker), `RUNNING`, and the terminal states `SUCCEEDED`, `FAILED`, `CANCELLED` and `EXPIRED`.

You can also send plain messages to a queue with `send()` when you don't need a task result.

### Claim

A **claim** gives one holder exclusive ownership of a named key inside a run, such as `company:acme` or `file:report.md`, for a limited time. Only one agent owns the key at a time; everyone else gets `CLAIM_HELD` or waits.

### Lease and fencing token

Ownership in runstate is always **leased**: a claim, a delivery taken from a queue, and units taken from a concurrency pool all expire unless the holder renews them. The SDKs renew automatically in the background (about every third of the lease) while you hold them, and stop the moment a renewal fails.

When a lease expires, the work goes back to the pool of available work, so a crashed agent can't hold it forever. Each lease comes with a **fencing token**. When the lease is lost, requests carrying the old token are rejected (`STALE_CLAIM`), so a slow or partitioned agent that comes back late can't overwrite the new owner's result. External systems get the same protection when they check the token.

### Holder and session

A **holder** is the name of a logical worker, stable across restarts (`holder: 'research-worker-3'`). A **session** is one live client instance; the SDK generates a fresh one every time you construct a client. Holders show up in the console and diagnostics, so give long-running workers meaningful holder names. If you don't set one, the SDK picks a random `worker-xxxxxx`.

### Capacity and money

- A **shared quota** is a fixed-window allowance of units (for example 100 search calls per minute) that every agent draws from. The API calls it an **allowance**; the SDK accessor is `run.quota(name)`.
- A **concurrency pool** is a counter of slots (browsers, GPU jobs) that agents lease. The API calls it a **permit**; the SDK accessor is `run.pool(name)`.
- A **work limit** bounds how much admitted work can be outstanding at once.
- A **budget** is an exact-decimal spend ceiling that agents reserve against, then settle.

### Completion

- A **task group** decides once whether a set of tasks is done: first accepted result, N accepted results, or all members finished.
- A **barrier** releases every waiter once N participants have arrived.
- A **timer** is a durable wakeup at a time or after a delay.

## Names in the docs vs. names in code

The docs use the words your team would use. The SDKs and the HTTP API keep their original names, and autocomplete shows those.

| In these docs | In the SDKs | In the HTTP API |
| --- | --- | --- |
| project / environment | `spaceId`, `RUNSTATE_SPACE_ID` | `/v1/spaces/{spaceId}` |
| run | `rs.scopes.create()`, `ScopeHandle`, `run.child()` | `/scopes` |
| ownership of a key | `run.claim(key)`, `ClaimRef`, `ClaimLease` | `/claims` |
| work queue | `rs.mailboxes.ensure()`, `run.mailbox(name)`, `MailboxRef` | `/mailboxes`, `/messages` |
| task with a durable result | `mailbox.submit()`, `TaskTicket` | `/tasks` |
| shared quota / rate limit | `rs.quotas.ensure()`, `run.quota(name)`, `AllowanceRef` | `/allowances` |
| concurrency pool / slots | `rs.pools.ensure()`, `run.pool(name)`, `PoolRef`, `PoolLease` | `/permits`, `/grants` |
| max in-flight work | `rs.workLimits.ensure()` (Python: `rs.work_limits`) | `/work-limits` |
| spend budget | `rs.budgets.ensure()`, `run.budget(name)`, `reserve` / `settle` / `void` | `/budgets` |
| "a task starts only when everything it needs is available" | `mailbox.admit()` | `/tasks/admit` |
| done when N good results | `run.groups.create()`, `'N_ACCEPTED'` | `/task-groups` |
| wait-for-all rendezvous | `run.barriers` | `/barriers` |
| durable wakeup | `run.timers` | `/timers` |

The shared quota has three spellings in code: the admin accessor is `rs.quotas`, the per-run accessor is `run.quota(name)`, and the class it returns is `AllowanceRef` (errors `ALLOWANCE_EXHAUSTED`, `ALLOWANCE_COOLDOWN`). They all refer to the same thing.

## Guarantees and their boundaries

runstate is precise about what it promises:

- **One recorded result per task.** A task has exactly one terminal outcome, even when several workers race. The work itself may run more than once if a worker crashes after doing it but before completing, so external side effects should be safe to repeat.
- **Stale owners are rejected.** After a lease is lost, the old holder's token no longer works in runstate. Systems outside runstate are protected only if they check the token.
- **Limits apply to work that goes through runstate.** Quotas, pools and budgets are a hard ceiling for every call your agents route through runstate. An agent that ignores runstate is not stopped by it.
- **Cancellation stops new work.** It does not kill processes; running agents see the cancellation on their next call and can hand back their work.
- **Waiting is fair where it is durable.** Shared quota waiting uses a durable FIFO queue on the server. Pool and claim waiting are client-side polling with jitter.

Everything else, such as process memory, prompts and model calls, stays in your code and your infrastructure.
