The entity is a shared facet
Roles that declare the same entity key form that entity's system. One dial per role, and every analytics query can scope to the whole journey.
The situation
A machine moves between three queues: the fleet queue, an unload station and a service bay. Each queue has its own role. Asking about the machine across all three should not mean listing the roles in every query.
The recipe
curl -X PATCH https://lt.acme.dev/api/roles/machine-fleet \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{ "entity_facet": "serialNumber", "entity_state_source": "subtype" }'
curl -X PATCH https://lt.acme.dev/api/roles/unload \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{ "entity_facet": "serialNumber", "entity_state_source": "role" }'machine-fleet entity_facet: serialNumber states from: subtype (idle, running)unload entity_facet: serialNumber states from: roleservice entity_facet: serialNumber states from: roleentity_facet names the metadata key for what moves through the role. entity_state_source says whether the role’s subtypes are states or the role itself is one state.
The guarantee
The system is derived, never maintained: query: { entity: 'serialNumber' } resolves server-side to every role declaring that key and implies exists: ['serialNumber']. An entity key no role declares is a 400 that names the gap. groupBy: { state: true } labels each row by its role’s dial. See Analytics.
Related patterns
Analytics
Park every state
Each state an item passes through is one escalation row. The queue becomes a complete time series of the process with no extra instrumentation.
Analytics
Dwell and membership
One call answers how long entities spent in each state over a window, or how many are in each state right now.
The physical world
The label and the row share a facet
A scheme names the metadata facet a label's target matches, so a printed code finds its row by the same value the workflow stamped.
Resolving from anywhere
Resolve by domain key
A caller that knows only orderId resolves the right row in one atomic statement, with no lookup of the escalation id first.
