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.

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 is the {connector} of providers.{connector}.{table}, and 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}, both lowercased; a disabled stream has no Provider table. An enabled artifact stream is served as artifacts.{name} unless the upload was published under another slug, and artifact_id names the upload behind it. 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 your Preferences from a Plot

workspace.preferences returns all records admitted by the current Workspace, Parable, actor, and version boundaries. It does not rank scopes or select one record per slot. A Person, Workspace, and authored record can all be visible for the same slot:
Select the desired id, or filter explicitly by scope, actor, slot, and ref/commit context. The id identifies the current stored row; rebuilding an authored projection may replace it. If a request names both a draft and a commit, their admitted records remain distinct rather than one winning. Existing SQL that assumed a scalar result per handle must now make its selection explicit. Other actors’ Person-scope rows remain hidden by the current access rule. Custom Protection sharing has not replaced that boundary yet. The legacy Preference SDK/API effective[] resolver also retains its scope fallback; this SQL projection no longer performs that selection. Reads are live: a Plot re-run after a Preference changes sees the new value, and a run records this table as unpinned. Because the rows depend on the person running the Plot, a scheduled Plot cannot read workspace.preferences and is refused at publish and schedule time.

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. Generated from psgen @projection declarations; edit the schema, not this block.

workspace.preferences

All visible Preference records; id identifies each record and refId/commitId distinguish simultaneously visible authored versions. Applications select values. Rows are scoped by the session settings platform.workspace_id, platform.parable_root_ref, platform.actor_id, platform.actor_kind, platform.preference_filter_protocol, platform.parable_ref, platform.parable_commit; 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. Because Workspace rows are read live, a scheduled Plot that references a workspace.* table records that input as unpinned in its run history: the run says what it read and when, and a replay reads the current state rather than the original rows. Use catalog discovery for the current table schemas and types and metadata before joining semantic values.