Skip to main content
The Workspace Pool is a live, query-safe projection of control-plane metadata. It is not a copied data-lake layer. Reads see the current Workspace state at query time.

Tables

Availability says whether a table is served today. A planned table is not referenceable yet: a query that names it fails at validation with PV_REF_002. Secrets, API tokens, sessions, and credential payloads are never exposed in this pool. Ids are served in their scalar’s spelling, base62 (YQJpYwUwvbaLOwTUr4thA), as in every Provider table and Plot, though Postgres stores them as uuid. An id typed into a comparison with an Identity.UUID column matches in either spelling, hyphenated or base62. The config tables’ tenant_connector_id uses the same spelling as workspace.providers.id.

List active Providers

Every connected Provider instance is a row, disabled ones included, so “active” is a predicate rather than a hidden rule:
connector_slug names the {connector} of providers.{connector}.{table} in catalog form: lower case, with each run of other characters as one underscore, so the claude-otel connector is served as providers.claude_otel. A join from Provider rows to this table folds the slug the same way. connection_status carries its enum values in the served schema. id is the tenant_connector_id of the per-connector config tables, so an instance’s settings and allowlist sit one join away:

List a Provider’s streams

Every stream configured on a Provider connection is a row, disabled ones included, so “enabled” is a column rather than a hidden rule. Join to workspace.providers on provider_id to name the connector and to drop the streams of disabled or deleted connections:
kind carries its enum values in the served schema. An enabled override stream is served as providers.{connector_slug}.{platform_tap_name} and an enabled custom stream as providers.{connector_slug}.{name}, the slug in the catalog form above and the name lowercased; a disabled stream has no Provider table. An enabled artifact stream for a legacy upload is served as artifacts.public.{published_slug} (also artifacts.{published_slug}), and artifact_id names the upload behind it. Publication-backed Artifact streams are omitted from provider_connections, including disabled streams: a publication table is served as artifacts.{namespace}.{table}, never as a stream, and is discovered through the Artifact catalog. Other streams of a disabled or deleted connection stay rows of provider_connections and drop out through the join above.

Roles are platform-level and scoped through membership

Roles are permission bundles defined once for the platform, not per Workspace. workspace.roles lists the roles that at least one member of the Workspace holds, with every membership status counted. Because the name is a display value an administrator can change, join on id, not name:
parent_name is NULL both when a role has no parent and when no member of the Workspace holds the parent; child.parent_role_id still carries the id in the second case.

Permissions cover their descendants

workspace.role_permissions lists the permission strings granted directly to each role in workspace.roles. A permission is a dotted path, and a grant covers every descendant of its path: a role granted tenant can do everything under tenant., including tenant.users.manage. The table does not expand that hierarchy, and it does not fold in the parent chain a role inherits through parent_role_id; both are one predicate away. To list the tenant-scoped permissions each held role grants:
To ask the question the other way round, which held roles cover a specific permission, compare the target against each grant plus a trailing dot:
Join on role_id, never on the role name, and join through workspace.roles to reach the parent chain when a role inherits from another. Which members hold a role is workspace.user_roles.

Who holds each role

workspace.user_roles is the assignment edge: one row per (member, role) pair. user_id joins workspace.users.id and role_id joins workspace.roles.id, so the members page is a three-way join:
Assignments are served for every membership status, SUSPENDED and REVOKED included, so filter on u.status = 'ACTIVE' to see who can act today. An assignment of a role that has since been deleted is still a row in workspace.user_roles; the join to workspace.roles drops it, because that table lists only live roles. assigned_by is the user ID of the person who made the assignment and may name someone who has since left the Workspace, so a LEFT JOIN to workspace.users is the right shape for it. To count members per role:

Read settings from a Plot

workspace.preferences returns the settings the person running the query can read for the selected Workspace and Parable: their own settings (Me), and the settings shared for This Parable or for a group whose Pane’s view.preferences Policy allows them. Each row is one setting: the Pane it belongs to (pane_key), its handle, its scope (actor_kind: person for Me, workspace for This Parable, group for a group setting, at any level of the Parable’s hierarchy, and actor_id, which for a group setting is the group’s ID in canonical form), and its value.
A person, each group and This Parable can each hold a setting under the same handle, so one handle can return several rows. The projection does not choose between them; filter on actor_kind when the query needs one. Other people’s settings are never returned. A Pane’s read is live: a Plot read again after a setting changes sees the new value, and the read records this table as unpinned. A Report run reads the settings as they were when the run was captured. Pressing Run, or a scheduled occurrence, stores the shared settings, This Parable’s and every group’s, on the run whenever a Plot of the published version reads workspace.preferences. Every Plot reads the captured rows whose Pane’s view.preferences Policy allows the run’s principal. Planning again reads the same captured values under the current grants. A person’s own settings are never captured, so in a Report run the table holds only rows with actor_kind workspace or group. The run’s pin set records the rows under snapshotInputs with the reason captured_settings, not as an unpinned live read. A run captured before settings were captured is refused; run the Report again. A Pane that shows a Report run reads the same captured rows. A Pane’s Plot that is not part of the Report can read the run’s stored outputs and workspace.preferences together, for example to apply the Workspace’s category corrections to a Report’s Meetings. In a Current reading it reads the settings live; when the reader picks a past Report run, it reads the settings that run captured, so a later setting never changes what the run showed. A run that captured no settings refuses that reading; run the Report again. Report runs also capture the Workspace directory projections their Version’s Plots read, for the live Plots a Page reads beside the run’s stored outputs. The generated projection SQL supplies the columns and Workspace predicates, and one bounded statement reads every directory, so they share a database snapshot. When a reader picks a past Report run, a live Plot reads each directory from the rows that run captured, under the same Workspace and Parable participation checks as its settings; a captured empty directory reads as empty. A run from before directories were captured, or one that did not capture a directory, reads current rows, and the Pane says Directory data is current, not as of this Refresh. A Plot with its own schedule or materialization has no Report run to capture settings, so publish and its schedule refuse it when it reads workspace.preferences. To read settings on a schedule, make the Plot part of a Report and schedule the Report. value is JSON text. Parse it with the JSON functions: json_get_str, json_get_int, json_get_float, json_get_bool, json_get_json, json_get_array, json_length, json_object_keys and json_contains, or the -> and ->> operators. json_get_array returns a list of JSON texts that unnest turns into rows. This Plot reads a shared category set, whose items are {id, parent, name, definition}, and the shared prompt, and asks Jev to place each Meeting in one of the set’s leaves. map() builds Jev’s criteria from the rows, so no JSON is written by hand:
A plan that reads quarantined Provider rows cannot call a JSON function (QL-PL-004): their raw record holds custom fields, which are read through the projected custom-field columns and the custom_fields MAP column.

Declared projections

Tables below are generated from psgen @projection declarations in platform-schemas/services/web-db: the same declaration produces the Postgres view, the Arrow schema the query layer serves, and this block. The docs site build fills the block; the page source keeps only its markers. Generated from psgen @projection declarations; edit the schema, not this block.

workspace.preferences

Every setting the principal may read for the selected Workspace and Parable line: their own settings, and the Workspace’s shared settings when they take part in the Parable. A Plot reads settings with plain SQL, for example SELECT value FROM workspace.preferences WHERE handle = 'categories'. Required request settings are workspace_id, parable_root_ref, actor_id and actor_kind; workspace.preference_can_view applies them. Rows are scoped by the session settings platform.workspace_id, platform.parable_root_ref, platform.actor_id, platform.actor_kind; the query layer sets them for every scan.

workspace.provider_connections

Streams of the current Workspace’s Provider connections: one row per tenant tap (an override of a platform tap, a custom tap, or an artifact-derived tap), joined to the platform tap it is bound to. Disabled streams are served with enabled = false so “enabled” is explicit in the consumer’s SQL; soft-deleted streams are not. Streams of a disabled or deleted connection are served too and drop out through the join to workspace.providers. The platform tap is reference data and is never filtered on its own soft-delete. Served by the query layer as workspace.provider_connections (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

workspace.providers

Connected Providers of the current Workspace: one row per connector instance, joined to the Provider definition it was created from. Disabled instances are served with disabled_at set so “active” is explicit in the consumer’s SQL; soft-deleted instances are not. The definition is global reference data and is never filtered on its own soft-delete: a deleted definition with live instances is an inconsistency worth seeing. Served by the query layer as workspace.providers (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

workspace.role_permissions

Permission strings directly granted to each role held in the current Workspace: one row per grant on a platform role that at least one member holds, whatever the membership status. Direct grants only: a role also inherits its parent chain (workspace.roles.parent_role_id), and a permission covers every dotted descendant of itself (‘tenant’ covers ‘tenant.users.manage’), so a coverage question is permission = ‘x’ OR starts_with(permission, ‘x.’). Grants of soft-deleted roles and of roles held only through soft-deleted memberships are not served. Served by the query layer as workspace.role_permissions (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

workspace.roles

Roles held in the current Workspace: one row per platform role that at least one member holds, whatever the membership status. Roles are platform-level permission bundles with no Workspace of their own, so a role no member holds is not served, and a parent_role_id may name a role that is absent from this table. Soft-deleted roles and memberships are not served. Served by the query layer as workspace.roles (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

workspace.user_roles

Role assignments for members of the current Workspace: one row per (member, role) pair, keyed by the person’s user id so it joins workspace.users directly and by role id so it joins workspace.roles. Assignments of every membership status are served, SUSPENDED and REVOKED included; join workspace.users.status to filter. Assignments of soft-deleted memberships are not served. Served by the query layer as workspace.user_roles (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

workspace.users

Members of the current Workspace: one row per membership, joined to the person’s profile. Every membership status is served, including REVOKED, so a query can reconcile who left; soft-deleted memberships and profiles are not. Served by the query layer as workspace.users (EDR-0081). Rows are scoped by the session settings platform.workspace_id; the query layer sets them for every scan.

Join live metadata to Provider data

workspace.users.email and Provider columns with the Contact.Email semantic scalar represent compatible values:
This query compares current Workspace membership with the most recently synced Provider directory state. Their freshness models differ: Workspace rows are live at query time, while Provider rows update when the corresponding stream syncs. A run on query-runner cannot read web-db when it executes, so it reads each workspace.* table once, when it plans the Plot, scoped to the run’s Workspace. The plan carries those rows, so every stage of the Plot reads the same rows. The Plot’s pin set lists the table under unpinnedInputs with reason captured and the time it read it; a restart from planning reads the table again. This holds for a Report run (see Reports) and for a scheduled run of a single Plot outside a Report, which has no person and reads each table as the Workspace. workspace.preferences is the exception: a Report run reads the settings it captured, and a Plot with its own schedule cannot read it (Read settings from a Plot). That read is bounded as a live one is: it fetches only the columns the Plot uses and, when the Plot’s LIMIT reaches the table, only those rows. A table past QUERY_LAYER_WEB_DB_MAX_ROWS rows is refused at planning when a live read of the same query would be refused. Use catalog discovery for the current table schemas and types and metadata before joining semantic values.