This document records the architecture, routing, settings hierarchy, migration behavior, and known gaps of Robin workspaces.
A workspace in Robin lists zero or more preferred repositories, and a repository is preferred by at most one workspace.
Preference decides where a repository's events run and what the workspace's sandbox image preloads; it does not limit access: every workspace's threads can use every repository the GitHub App installation can reach.
The workspace absorbs the former environment (prompt, snapshot, setup and update scripts, sandbox sizing, nightly refresh) and additionally owns the Slack channels that route to it, its MCP connections, and workspace-scoped settings.
Inbound work is routed to a workspace by its thread, then a tag, then its Slack channel, then its repository's preferred workspace, then the user's default, and otherwise to the default workspace.
Every signed-in user sees every workspace, and every admin administers every workspace; workspace roles remain future work.
The OSS team wants Robin to handle issues on public repositories. Before workspaces, one deployment was one scope: the same settings, MCP connections such as Datadog, and installation-wide GitHub access served every run. Trigger gates keep outsiders from starting runs, but extending the same scope to public repositories means public content meets internal access. Deploying a second instance avoids that but is heavyweight for what will be a common split.
Two concepts already point at the answer. Environments carry a prompt, a snapshot, and a list of repositories, and are selected per thread. MCP connections and thread visibility are already called "workspace" scoped. The workspace is a first-class object and folds the environment into it, since the two would otherwise map one to one.
A workspace has an immutable slug and a display name, plus:
- Preferred repositories.
Zero or more
owner/nameentries. A repository is preferred by at most one workspace; saving a workspace that claims a repository another workspace prefers fails with a conflict, so moving a preference is an explicit edit. A preferred repository's GitHub events, Linear issues, and automations run here, and its binding carries repository settings such asmay_start_threads. Preferring a repository is not what grants access to it. - Environment fields, moved from the environment record unchanged: prompt, base snapshot, setup and update scripts, sandbox resources, create parameters, captured snapshot state, and the refresh schedule. Snapshot names keep their current form so existing snapshots stay valid.
- Slack channels. Zero or more channel ids whose messages route here. A channel routes to one workspace.
- MCP connections of its own, layered over the instance-wide set that existed before workspaces (see "Settings tiers").
- Settings, the former team settings: model defaults, review toggles, organization guidelines, gateway and Fable toggles, default repository. These are tiered; see "Settings tiers" below.
Instance-level configuration stays in the environment: the GitHub App and its installation, the Slack app, model provider keys, sandbox provider and sandbox credentials, CONFIGURED_ADMINS, the sign-in allowlist, and the base snapshot fallback.
Settings resolve from the least to the most specific tier, each overriding the one before:
- Instance. The settings record as it existed before workspaces (then "team settings"), kept under its original Store key so an upgrade needs no migration. Admins edit it on the Admin page.
- Workspace.
A sparse record of overrides per workspace slug.
A field that is unset inherits the instance value, so a new workspace behaves exactly like the instance until an admin changes something.
GET /dashboard/api/workspaces/{slug}/settingsreturns the effective values and the overrides separately;PUTreplaces the overrides, and anullfield means "inherit". - User. Profile preferences such as default repository and draft-PR review apply to that user's runs. Model, reasoning effort, subagent model, and adaptive-routing defaults come from workspace settings, not personal profiles.
- Thread.
A run's
configurable(agent_model_id,agent_effort,repo) overrides all of the above for that thread.
MCP connections follow the same shape without a thread tier: instance connections (/dashboard/api/mcps) are loaded for every run, the workspace connections are added, then the user's personal connections.
A later tier's connection replaces a same-named one from the tier before, which is how a workspace or a user swaps in different credentials for a shared server.
The connections configured before workspaces existed are the instance tier: they keep applying to every workspace, as they always did.
A workspace may use any accessible repository as its default repository, including one another workspace prefers. Existing GitHub user and installation access checks remain in place.
Workspaces, and the repository and Slack-channel bindings that route to them, live in PostgreSQL: a workspace table (one row per workspace, slug unique) plus workspace_repository and workspace_slack_channel, which key on the bound resource (repository_id, referencing the repository table, and channel_id), so the database itself enforces that a repository is preferred by, and a Slack channel bound to, at most one workspace.
Settings and MCP connections live in their own PostgreSQL tables, keyed by workspace slug.
Workspace, WorkspaceCreate, and WorkspaceUpdate remain the domain and API shape that the dashboard, the agent tools, and routing read and write; the tables are an implementation detail behind WorkspaceStore, swappable again without touching a caller.
Every thread records its workspace at creation and never changes it. Resolution for new work, in order, and the first match wins:
- An existing thread's recorded workspace. Follow-ups on issues, PRs, Linear issues, and Slack threads land here; none of them recompute the workspace from the repository.
- A
/workspace:<slug>command on the message that opens the thread, e.g.@Robin /workspace:infra fix the flaky test. The olderworkspace:<slug>andenv:<slug>tags still work anywhere in the message. - The Slack channel's bound workspace. A message in a workspace's channel runs there even when it names a repository another workspace prefers, since every workspace can use every repository.
- The repository's preferred workspace.
This is how GitHub events, Linear issues, automations, and GitHub Actions federation (through the binding's
may_start_threads) pick a workspace. - The user's default workspace, a per-user preference.
- The
defaultworkspace. Instance policy decides what happens to a GitHub event for a repository no workspace owns: route it todefault, which is the compatible upgrade behavior, or drop it, which a locked-down install should prefer.
Coding sandboxes normally get a token for the whole GitHub App installation, not just the workspace's preferred repositories. Threads started by a GitHub event or an issue automation on a public repository record that repository in their metadata when created, server-side and never from run configuration, and receive a token narrowed to that repository, including on every proxy refresh. Unknown repository visibility also causes repository-scoped access. Reviewer and review-scout sandboxes explicitly scope their tokens to the repository being examined. Coding threads that members start from Slack, the dashboard, or Linear normally get the installation-wide token, as do workspace image builds.
A workspace's preferred repositories are what its image preloads: the setup and update scripts receive them in OPENSWE_WORKSPACE_REPOS, clone each to /workspace/<repo>, and changing them rebuilds the image.
Before an update script runs, Robin clones any missing preferred repository into /workspace/<repo>, so an older image can catch up with the current repository list.
Existing paths are left untouched for the script to validate; a clone failure fails the update and preserves the previous image.
Any other repository is cloned on demand to the same place by the run that needs it.
A refresh writes only snapshot and refresh state and an edit writes only the definition, so neither reverts the other.
Admins configure AWS, GCP, and Azure workload identity in Workspaces → Sandbox Proxy. These are nonsecret federation profiles, stored alongside the existing sandbox creation settings: AWS role ARN, GCP workload identity provider and optional service account email, or Azure tenant/client UUIDs and optional resources. Only one rule per cloud is allowed, and federated rules must remain enabled. The JSON editor and the admin workspace API/tools edit the same configuration; no cloud keys or client secrets are accepted. Existing non-federated cloud rules keep their JSON contract, subject to the existing secret restrictions; federation-only validation does not require those rules to be enabled.
Every federated rule, and any federated mount auth, must use a governed tag scope, tag:<tag value UUID>.
Every Robin workspace creates sandboxes in the same LangSmith workspace, so a workspace scope would let all of them use the role; Robin rejects it.
Add the existing tag value UUID to Governed Tag Value IDs and use the same UUID in the rule's subject scope.
Before enabling a profile, configure cloud trust for the LangSmith deployment's OIDC issuer and the exact subject ws:<LangSmith workspace UUID>:sandbox:tag:<tag value UUID>.
The LangSmith workspace UUID is not the Robin workspace UUID or slug.
Robin validates UUIDs and scope membership; LangSmith validates tag ownership and the deployment API key's assignment permissions when creating the sandbox, and rechecks tag scope on credential refresh.
Saving a profile does not verify that the cloud will accept it; invalid cloud trust fails sandbox creation.
Cloud permissions apply to every matching guest request, not just filesystem mounts. Grant only the required cloud permissions, without unnecessary role-chaining or service-account impersonation rights. Credentials are exchanged and applied by the sandbox host proxy, not stored in the image or passed to the guest. The LangSmith deployment and sandbox hosts must support workload identity and have their issuer/signing keys configured first.
Image inheritance shares the default workspace's snapshot, sizing, and update tracking, not its creation settings.
Proxy rules, mounts, environment variables, and tag assignments remain explicit on the child workspace.
This changes the earlier behavior that inherited all creation settings: configure any settings the child needs directly.
An inherited image can still contain files or credentials written by its build scripts; only share images with trusted workspace users.
Deployment-wide SANDBOX_CREATE_EXTRA_JSON remains an operator-controlled default for every workspace, including cloud authority; do not configure it with global cloud rules or tag assignments when workspace separation is required.
Federated authority is included when a new sandbox is created and preserved while GitHub and tool credentials refresh. Workspace image builders use the workspace's same explicit creation settings. Saving or removing a profile does not modify existing sandboxes: start a new thread or explicitly recreate its sandbox to apply changes. Recreation discards the old sandbox's filesystem, so preserve work first; removing cloud trust or the governed tag in LangSmith is the separate revocation mechanism for existing sandboxes.
LangSmith Access in the same section lists LangSmith permissions (for example projects:read, datasets:read) that code in new sandboxes may use against the LangSmith API.
Robin saves them as an explicit access_delegation grant; the sandbox proxy injects a short-lived credential, so no key enters the sandbox.
The delegator is Robin's own sandbox-creating LangSmith key, not the person running the thread, so every thread in the workspace gets the same access, capped by what that key holds.
INHERIT grants and tenant administration (workspaces:manage*, organization:manage*, organization:pats:*) are refused.
Like cloud identity, the grant is fixed when a sandbox is created.
Every signed-in user may view and use every workspace, and every configured admin may administer every workspace. Public threads remain visible to all signed-in users and private threads to their owner and admins, as today. Role-based access per workspace is explicitly deferred.
The Environments page becomes the Workspaces page, with repositories and Slack channels editable.
The composer picks the workspace first and the repository second: every workspace lists every accessible repository, and choosing a workspace preselects its default repository.
A repository named from outside — a link or the profile default — selects the workspace that prefers it.
The dashboard has no separate "project" notion: the sidebar nests each repository folder under the workspace that prefers it, and default for one no workspace prefers.
Admin settings and MCP connections gain a workspace selector.
Existing environment records become workspace records with the same slugs.
The current team settings and MCP connections become the default workspace's.
Existing threads without a workspace read as default.
The environment key in thread metadata and run configuration is read as an alias for workspace so in-flight threads keep working.
Workspaces live only in PostgreSQL.
Robin no longer reads the LangGraph Store; a deployment still holding records there must move them with an earlier release first (see Upgrading from the LangGraph Store).
A migration seeds the default workspace, so pickers always offer it.
- Per-workspace roles or membership. Everyone sees everything in this version.
- Per-workspace GitHub or Slack apps. One App and one Slack team per instance.
- Routing individual Slack messages within a channel to different workspaces.
- Per-workspace memory. The existing per-user memory stores are unchanged for now.
- A per-workspace MCP allowlist for externally triggered runs and public-safe prompts and outputs.
Partitioning settings by workspace removes the accidental path from a public repository's events to internal MCP connections, guidelines, and prompts, provided the public repositories are preferred by a workspace that has none of them. GitHub access is not partitioned: every sandbox token covers the whole installation, so the token is not what isolates a workspace. Threads started by GitHub events or automations on public repositories, where outsiders' content is most likely, get a token for that repository only; the reviewer and review scout keep their own single-repository tokens. A member-started thread can reach every installation repository, including ones the member cannot access on GitHub themselves. Unassigned repositories are routed by an explicit instance policy rather than silently defaulting, so a locked-down install can drop them. No new credentials are introduced, and MCP connection secrets stay encrypted under the workspace they belong to.
- Deploy a second instance for OSS. Works today with no code, but doubles operations for a split that many teams will want, and gives no shared dashboard or identity.
- Multiple GitHub Apps, one per workspace. Couples sign-in and webhook secrets to workspaces and requires separate credentials. Shared bindings work with one installation.
- Workspaces bound to installations rather than repositories. Simpler routing, but one GitHub organization then cannot host more than one workspace, which is the common case here.
- Repository access scoped per workspace, with repositories shared between workspaces. Kept the token narrow, but made every repository a workspace wanted to touch an access decision and a routing decision at once, and left shared repositories' events with no clear owner.
- Keep environments separate from workspaces. Two objects with a one-to-one mapping would only add a join. Folding the environment in keeps a single admin surface.
- Should memory become per workspace, per user and workspace, or stay per user?
- Confirm the follow-up scope for public workspaces listed under non-goals.
Known gaps this design leaves open, so they survive outside the pull requests that built it.
- A renamed or transferred repository keeps its old binding.
An inbound webhook is matched to a
repositoryrow byrepository.key(lowercasedowner/name), and nothing updates an existing row's name, so the first event for the new name inserts a second row while the workspace binding stays on the old one. The practical remedy today is to re-save the workspace's repository list with the new name, which rebinds it; underOPEN_SWE_UNASSIGNED_REPO_WORKSPACE=ignorethe deliveries in between are answered 200 ignored and never retried. A reconciliation job that follows GitHub'srepository.renamedevent would close it. - A corrupt workspace row is skipped rather than repaired. A record an older release wrote that no longer validates is left out of the listing and reads as missing, logged at error. Nothing reports it to an admin, and there is no way to fix it from the dashboard.
- Concurrent edits to one workspace overwrite each other.
apply_updatereads a record, applies the patch, and writes the whole row back, so the last save wins; two admins editing the same workspace need optimistic concurrency onupdated_atto notice. - The public-workspace lockdown. A per-workspace MCP allowlist for externally triggered runs, and public-safe prompts and outputs are what a genuinely public workspace needs beyond partitioning. That is the next proposal, and it depends on this one.
- The Linear default repository reads the default workspace.
A Linear issue with no repository in it falls back to
default's configured repository rather than the resolved workspace's, unlike the Slack path, which scopes a defaulted repository to the workspace that won. - The sidebar files a repository under its preferred workspace only.
A thread that ran in another workspace on that repository still appears in the preferred workspace's folder.
Grouping by each thread's own workspace needs
/threads/reposto return one entry per repository and thread workspace, a workspace filter on/threads/page, and folder keys (and the pin and collapse preferences stored under them) that include the workspace. - Unfinished dashboard pieces. The review page's guidelines and toggles are per workspace, but the repository list above them is still every installed repository grouped by GitHub owner, which does not say which workspace each one belongs to.