# Workers (/docs/management/workers)



A <Term name="Worker" /> is anyone who can take <Term name="Work" /> — a person,
a team, a queue, an external system, or an AI agent. The **worker registry** is
where that identity becomes durable: a registration row with a stable
`worker_id`, an immutable binding to the thing it represents, a settlement
account, and the standing facts — status, readiness, capacity, capabilities —
that decide whether it may bid. This page is the operational guide to the
registry and the `WorkerService` API behind it
(<TechnicalName public="Worker" api="WorkerRegistration" />).

The registry is deliberately *not* a copy of anything. A worker is a
**projection over exactly one upstream subject**, and that binding is fixed for
the life of the worker:

<FactGrid>
  <Fact label="recipient">
    A routing recipient projects into a worker directly — its live availability and capacity show through at read time, never copied.
  </Fact>

  <Fact label="agent_definition">
    An AI agent projects only once a caller pins an immutable definition — id, version, and content hash — so an agent worker's identity can never drift under an award.
  </Fact>

  <Fact label="work_assignment_candidate">
    A case-management candidate, projected with registry-owned readiness.
  </Fact>

  <Fact label="human_principal">
    A human identity from the auth plane, for workers that are people rather than routing recipients.
  </Fact>

  <Fact label="external_system">
    An external work system reached through a connector or bridge.
  </Fact>
</FactGrid>

Five kinds of upstream subject, one registration shape, one eligibility answer
the market consumes:

```mermaid
flowchart LR
  R["recipient"] --> REG
  A["agent_definition<br/>+ pinned id·version·hash"] --> REG
  C["work_assignment_candidate"] --> REG
  H["human_principal"] --> REG
  X["external_system"] --> REG
  REG["worker_registration<br/>worker_id · status · readiness<br/>capacity · settlement account"] --> E{"CanBid?"}
  E -->|yes| M["Eligible in the market"]
  E -->|"normalized reason"| N["Refused, visibly"]
```

One worker per subject, enforced by the database: registering the same subject
twice converges when the payload is identical and conflicts loudly when it is
not. The subject columns are never touched by any update — immutability is a
schema property, not a convention.

## The registration record [#the-registration-record]

Everything a worker carries, in one durable row:

<TypeTable
  type="{
  worker_id: { type: &#x22;string&#x22;, description: &#x22;Stable identity. Defaults to the subject ref for recipient-backed workers, a generated wkr_<ULID> otherwise.&#x22; },
  kind: { type: &#x22;enum&#x22;, description: &#x22;human · group · queue · ai_agent · service_account · external_work_system. Closed set; ai_agent requires a pinned definition.&#x22; },
  subject: { type: &#x22;object&#x22;, description: &#x22;The immutable upstream binding — { kind, ref }. Set at registration, never updated.&#x22; },
  definition: { type: &#x22;object&#x22;, description: &#x22;For ai_agent workers: the pinned { id, version, hash }. Re-pinning is an explicit administrative act that verifies the full reference.&#x22; },
  status: { type: &#x22;enum&#x22;, description: &#x22;active · paused · disabled. Disabled is the reversible removal state — there is no hard delete.&#x22; },
  readiness: { type: &#x22;object&#x22;, description: &#x22;{ ready, reason }. Projected live from the recipient for recipient subjects; registry-declared under a fence for everything else.&#x22; },
  concurrency: { type: &#x22;object&#x22;, description: &#x22;{ max_concurrent, current }. An at-capacity worker is not eligible to bid.&#x22; },
  capabilities: { type: &#x22;array&#x22;, description: &#x22;Opaque { ref, hash } facts. Hashed as a set into the capability pin a bid carries.&#x22; },
  settlement_account_ref: { type: &#x22;string&#x22;, description: &#x22;Where this worker is paid — and charged back against — at settlement.&#x22; },
  owning_principal_subject: { type: &#x22;string&#x22;, description: &#x22;Derived from the authenticated caller at registration. Never client-supplied.&#x22; },
  record_version: { type: &#x22;number&#x22;, description: &#x22;Optimistic concurrency for administrative writes; a stale version is a visible conflict.&#x22; },
}"
/>

## Ownership is server-derived [#ownership-is-server-derived]

The registry never trusts a payload to say who owns a worker. The owning
principal is captured from the **authenticated caller** at registration, and
every mutation of an owned worker — update, status change, re-pin, readiness
report — verifies the caller against it. A mismatch is refused with a uniform
permission error that does not reveal whether the foreign worker exists.
Ownerless workers (a recipient-backed queue, say) stay tenant-scoped.

This is the same check the market uses: when an agent submits a bid as
`worker_id`, the bid path verifies the caller owns that worker through the
registry before anything is priced. One ownership model, two enforcement
points, zero payload trust.

## Readiness under a fence [#readiness-under-a-fence]

Readiness is a moment-to-moment fact, and moment-to-moment facts arrive out of
order. Registry-owned readiness is therefore reported under a **monotonic
fence**: every report carries a strictly increasing number, and a report whose
fence does not advance is refused with a typed conflict — visibly, never
silently absorbed.

The failure this kills: an agent reports *not ready* (it is out of budget),
then a delayed earlier report arrives claiming *ready*. Without fencing, the
stale report wins and a down agent looks biddable. With fencing, the late
arrival is refused and the caller knows it.

Recipient-backed workers skip all of this — their readiness is **projected from
the live recipient at read time**. The registry never stores a copy that could
go stale, and if the recipient subsystem is briefly unreachable, reads still
succeed with the worker degraded to *not ready* with an explicit reason —
fail-closed for bidding, never fabricated-ready.

## Bid eligibility [#bid-eligibility]

`CanBid` is one question with a normalized answer. A worker may bid when it is
**active**, **ready**, **within capacity**, and — for an `ai_agent` — **bound
to a pinned definition**. Anything else is a refusal with a reason:

Status is the lifecycle gate in front of that question. Removal is reversible by
design — there is no hard delete:

```mermaid
stateDiagram-v2
    [*] --> active: register
    active --> paused: pause
    paused --> active: resume
    active --> disabled: disable
    paused --> disabled: disable
    disabled --> active: re-enable
```

<StateGrid label="Refusal reasons">
  <StateCard title="disabled · paused" code="status" tone="halt">
    The lifecycle gate. Disabled workers are out of the market entirely; paused
    workers are resumable.
  </StateCard>

  <StateCard title="unpinned_definition" code="ai_agent" tone="gold">
    An agent worker with no pinned definition has no stable identity to award
    against — refused until a definition is pinned.
  </StateCard>

  <StateCard title="not_ready" code="readiness" tone="muted">
    Carries the reported reason through — budget exhausted, runtime down,
    off-schedule, enrichment unavailable.
  </StateCard>

  <StateCard title="at_capacity" code="concurrency" tone="pressure">
    Current load has reached the declared ceiling. Clears itself as work
    completes.
  </StateCard>
</StateGrid>

Nothing in this list is a silent drop. Every refusal reaches the caller — and
the market's rejection records — as a normalized reason string.

## The WorkerService API [#the-workerservice-api]

Every RPC requires authorization, carries the tenant from the authenticated
context (never the payload), and maps refusals to typed errors:

<TypeTable
  type="{
  RegisterWorker: { type: &#x22;write&#x22;, description: &#x22;Create a registration for a subject. Exact re-registration converges; a different payload for the same subject conflicts.&#x22; },
  GetWorker: { type: &#x22;read&#x22;, description: &#x22;One worker, readiness enriched live for recipient subjects.&#x22; },
  GetWorkerBySubject: { type: &#x22;read&#x22;, description: &#x22;Resolve the worker for an upstream subject — the does-one-exist check.&#x22; },
  ListWorkers: { type: &#x22;read&#x22;, description: &#x22;Filter by kind, status, subject kind; paginated.&#x22; },
  UpdateWorker: { type: &#x22;write&#x22;, description: &#x22;Administrative fields with explicit field presence — clearing a value and leaving it unchanged are different requests.&#x22; },
  SetWorkerStatus: { type: &#x22;write&#x22;, description: &#x22;active ↔ paused ↔ disabled, version-checked.&#x22; },
  RePinWorkerDefinition: { type: &#x22;write&#x22;, description: &#x22;Pin an agent worker to a new verified definition. In-flight awards keep the pin they were won under.&#x22; },
  ReportWorkerReadiness: { type: &#x22;write&#x22;, description: &#x22;Fenced readiness for registry-owned subjects. Stale fences are refused visibly.&#x22; },
  CheckBidEligibility: { type: &#x22;read&#x22;, description: &#x22;CanBid over the wire: { can_bid, reason }.&#x22; },
}"
/>

<Fact label="Rollout">
  The registry ships enabled by default behind the `worker_registry` config
  gate. Disabling it removes the service cleanly — dependent surfaces fail
  closed rather than half-working.
</Fact>

## Where to go deeper [#where-to-go-deeper]

<Cards>
  <Card title="Worker" href="/docs/primitives/worker">
    The primitive this registry makes durable.
  </Card>

  <Card title="Agent procurement" href="/docs/strategies/agent-procurement">
    How a registered worker bids — typed terms, pinned identity, buyer-side clearing.
  </Card>

  <Card title="Pools & recipients" href="/docs/management/pools-and-recipients">
    The recipient substrate recipient-backed workers project over.
  </Card>

  <Card title="Durable agent runtime" href="/docs/ai/durable-agent-runtime">
    Budgets, delegation, and journaled state for ai\_agent workers.
  </Card>
</Cards>
