Use workspaces and clean rooms
A workspace is the universal ownership boundary, every asset, table, message, model, and job belongs to one. A clean room is a mutual-distrust governance overlay laid on a workspace, so parties can compute jointly without trusting each other with raw inputs. This guide is the practical path through both: create, populate, invite, govern, and promote a result.
Operations run against the node
admin surface (/api/public/v1/admin/…, authenticated session, the console drives
the same endpoints). Workspaces also have a s9-admin CLI.
The flow at a glance
Figure: the workspace-and-clean-room flow. A workspace (green) is created and given members; a clean room (green) is laid on a host workspace, collaborators are invited, assets contributed (seen only as aliases), a notebook approved under M-of-N review (cyan), a run executed through the fail-first egress gate (amber), and the result promoted to a data product (filled). Ceiling: a clean room is policy-based isolation with a signed, hash-chained run audit, not a hardware trusted execution environment.
Table equivalent
| Step | Operation | Key inputs |
|---|---|---|
| Create workspace | POST /workspaces / s9-admin ws create | name |
| Add member | POST /workspaces/{id}/members / s9-admin ws members add | entity_id, role |
| Create clean room | POST /cleanrooms/ | name, owner_entity_id, governance |
| Invite collaborator | POST /cleanrooms/{id}/collaborators | entity_id, role |
| Contribute asset | POST /cleanrooms/{id}/assets | contributor_entity_id, source_fqn, alias |
| Approve notebook | POST /cleanrooms/{id}/notebooks/{id}/approve | reviewer authority |
| Run | POST /cleanrooms/{id}/runs | proposed_epsilon_burn |
| Promote | POST /data-products/from-clean-room | product fields + governance |
Part 1: Workspaces
Create a workspace
Every entity is auto-provisioned one workspace at creation, but you create
additional ones for teams, projects, and environments. name is the only required
field:
# CLI
s9-admin ws create acme-analytics --description "Analytics team workspace"
# API equivalent, type. user | agent | shared | system; environment: prod | staging | dev | sandbox
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/workspaces \
-H "Content-Type: application/json" \
-d '{"name":"acme-analytics","type":"shared","environment":"prod","description":"Analytics team workspace"}'
Creating a workspace mints the whole environment in one step, a dedicated storage namespace, a messaging inbox, a feed, default streams, and an owner membership row for you. Isolation is structural, not a name prefix.
Add members and roles
Access is by membership role (owner, editor, contributor, viewer) or
a group grant, enforced at the gateway:
s9-admin ws members add <workspace_id> --entity <entity_id> --role editor
s9-admin ws members list <workspace_id>
# API equivalent
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/workspaces/<workspace_id>/members \
-d '{"entity_id":"entity:bob","role":"editor"}'
Move a workspace across cells
A workspace is a whole, portable environment. Clone, migrate, snapshot,
archive, and fail it over across cells with the
Workspace Mobility
operations (clone, cross-org-clone, archive/restore, DR failover/failback),
also on the workspace admin router. See
Migrating a workspace
for clone-vs-migrate semantics.
Part 2: Clean rooms
A clean room is the workspace console, scoped to the room, with collaboration governance layered on. Everything you can do in a workspace you can do in a clean room, with the guarantees built in.
Create a clean room
Create it on a host workspace. name and owner_entity_id are required; the rest
are the governance dials, each with a safe default:
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/ \
-H "Content-Type: application/json" \
-d '{
"name":"acme-partner-overlap",
"owner_entity_id":"entity:acme",
"workspace_id":"<host_workspace_id>",
"min_aggregation_threshold":50,
"dp_budget_per_collaborator":1.0,
"output_review_required":true,
"approval_threshold":2,
"region_constraint":"eu",
"purpose":"audience overlap for a joint campaign"
}'
Key dials: min_aggregation_threshold (default 50) sets the k-anonymity floor;
dp_epsilon_max / dp_budget_per_collaborator set the differential-privacy
budget; output_review_required forces human output review; approval_threshold
(default 1) is the M in M-of-N notebook approval; region_constraint pins where
the room may run. A new room is deny-all until grants are made.
The governing permission scopes are catalog.cleanroom.create, .admin,
.contribute, .review, and .run, separation of duties is built into the
model.
Invite collaborators
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/<room_id>/collaborators \
-d '{"entity_id":"entity:partner","role":"analyst"}'
Contribute assets: seen only as aliases
Each party contributes its own data; a collaborator only ever sees a per-room alias, never the source table name (notebook bodies are alias-rewritten before compute, so the kernel never sees a source name):
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/<room_id>/assets \
-d '{"contributor_entity_id":"entity:acme","source_fqn":"analytics.sales.orders","alias":"party_a_orders"}'
Approve a computation and run it
Notebooks are attached, submitted, and then approved under M-of-N review, an edit resets approval. Only an approved notebook runs, and every output passes the egress gate:
# Attach + submit a notebook, then approve (needs approval_threshold reviewers)
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/<room_id>/notebooks -d '{...}'
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/<room_id>/notebooks/<nb_id>/approve
# Run, declare the DP budget the run may burn
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/cleanrooms/<room_id>/runs \
-d '{"notebook_id":"<nb_id>","proposed_epsilon_burn":0.1}'
The egress gate: an ordered, fail-first cascade
Every output passes through an ordered cascade, and the first failure terminates the run. No output leaves except through it:
| Order | Check | On failure |
|---|---|---|
| 1 | k-anonymity | Terminate |
| 2 | Max-output-rows cap | Terminate |
| 3 | Per-collaborator differential-privacy budget | Terminate |
| 4 | Per-run epsilon ceiling | Terminate |
| 5 | Destination allow / deny (default deny) | Terminate |
| 6 | DLP inspection | Terminate |
Adjust the destination allow/deny list with PUT /cleanrooms/{id}/egress-policy.
A clean room is policy-based isolation with a signed, hash-chained run audit, not a hardware trusted execution environment; the tamper-evident run chain is the documented compensation for not having an enclave. A workspace is fully mobile across cells, but a clean room is single-cell for cross-cell recovery today, the room-clone primitives and fail-closed cutover admission gate are real, while carrying an in-flight run's DP budget and output-review posture across cells is in progress.
Promote a clean-room result
The commercially interesting end of an engagement: a joint result computed under mutual distrust becomes a first-class, owned, contract-bearing asset:
curl -X POST https://CELL_HOST:8443/api/public/v1/admin/data-products/from-clean-room \
-d '{"room_id":"<room_id>","name":"overlap_summary","version":"1.0.0","domain":"marketing","owner_entity_id":"entity:acme","team_id":"team:growth","governance":{"classification":"CONFIDENTIAL"}}'
See Set up data-platform resources → data products.
Verify it worked
For a workspace:
| # | Check |
|---|---|
| 1 | Members hold the roles you set, and the resolved capability set matches |
| 2 | Bound catalogs appear with the right privilege |
| 3 | The activity record shows your actions on a hash-chained timeline |
For a clean room, four more that matter:
| # | Check | Why |
|---|---|---|
| 4 | Every contributed asset shows its alias, never a source name | The alias is the contract surface |
| 5 | The egress policy defaults to deny | Denial always wins over the allow list |
| 6 | A notebook must be approved before it runs | And editing it resets that approval |
| 7 | The privacy budget moves when a run completes | It is a ledger that gates, not a report |
Check 7 is the one to test deliberately. Run something, then confirm the budget went down and stayed down.
When it does not work
| Symptom | Most likely cause | What to do |
|---|---|---|
| A collaborator sees nothing | They are not an active collaborator, or nothing is contributed yet | Reads are deny-closed on membership |
| A notebook will not run | It is not approved, or an edit reset its approval | Approvals are keyed to a version hash. Re-review the new version |
| A query returns fewer rows than expected | The anonymity threshold refused the under-sized group | Aggregates flow, raw and under-threshold rows do not. Working as intended |
| A result will not leave the room | The destination is not on the allow list | Default deny. Add the destination or change the default deliberately |
| The privacy budget is exhausted | Consumption reached the ceiling | It does not come back. Deleting a run does not restore it, because the disclosure already happened |
| A tool is missing that the docs describe | You are in the narrower catalog view of the room | See clean-room governance for the two entry points |
Across the deployment matrix
- Single-cell: a workspace or clean room is a local boundary the engine enforces on every read; a clean room runs on one cell.
- Multi-cell: a workspace boundary spans cells and the whole environment is mobile (clone, migrate, snapshot, archive, failover); clean-room cross-cell in-flight recovery is in progress.
- Sovereign / air-gapped and cloud-enabled: boundary enforcement is a property of the local engine, so it holds identically with or without connectivity; a clean room needs no vendor cloud to run.
Related
- Workspaces and clean rooms, the model behind these operations.
- Workspace mobility: clone, migrate, snapshot, archive, failover.
- Governed reads, the row filters and masks a workspace read carries.
- Set up data-platform resources: the catalogs, products, and connectors a workspace holds.