Role is the wall, facets are capability
The role decides who can ever see a row. Metadata facets decide which consumer inside that role can take it.
The situation
Machining work is visible only to the machining team. Inside that team, some cells cut aluminum and some cut steel, and some take oversize parts. Routing needs both a hard boundary and soft preferences.
The recipe
const done = await conditional<{ ok: boolean }>(`machine-${serialNumber}`, { role: 'machining', priority: 2, metadata: { serialNumber, material: 'aluminum', sizeClass: 'standard', mustCompleteBy: deadlineMs },});const { data } = await lt.escalations.searchByFacets({ role: 'machining', status: 'pending', available: true, facets: { material: 'aluminum' }, block: [{ sizeClass: 'oversize' }], orderBy: [{ field: 'metadata.mustCompleteBy', numeric: true, direction: 'asc' }], limit: 10,});| Concern | Expressed as |
|---|---|
| Capability, hard | role |
| Capability, soft | facets containment, block exclusions |
| Preference | orderBy over columns and metadata.<key> |
| Capacity | the limit of a claim |
The guarantee
A consumer bound to one role can never see another role’s rows; a role it does not hold matches nothing. Facets are GIN-served containment, and every value is a bound parameter. Only validated keys and a fixed operator set are interpolated. See Faceted routing.
Related patterns
The queue as a market
Scope is enforced in SQL
A member's read and write scope are ANDed into the query itself, so totals, pages and claims all respect it and nothing is filtered after the fact.
The queue as a market
Availability is a query
A row is held only while its claim window is live. Availability is computed at read time, so no job has to flip a flag back.
The queue as a market
Two-sided dispatcher
Demand and supply are both rows. A dispatcher counts free supply, claims that much demand, locks the supply all-or-none, and pairs them.
Agents and MCP
Scope times role
A key's scope decides read or write; the account's roles decide which tools and which records. Both must pass on every call.
