workspace.json
Each workspace has its own config file at <workDir>/workspaces/<wsId>/workspace.json. This is where connectors, skill directories, and optional model overrides live — not in nimblebrain.json.
Workspaces are created and managed from the settings pages, which call the app-only nb__manage_workspaces tool (the agent cannot); their files are maintained by the runtime. Workspace ids are opaque and name-independent; every new workspace gets a generated ws_<16-hex> id, and no caller can choose one.
Example
Section titled “Example”{ "id": "ws_a1b2c3d4e5f60718", "name": "Product", "about": "Product team workspace — roadmap, metrics, and customer research.", "members": [ { "userId": "usr_default", "role": "admin" }, { "userId": "usr_alex01", "role": "member" } ], "connectors": [ { "url": "https://mcp.granola.ai/mcp", "serverName": "ai-granola-mcp" }, { "url": "https://mcp.example.com/mcp", "serverName": "example" } ], "skillDirs": ["./skills", "/opt/nimblebrain/skills"], "models": { "default": "anthropic:claude-opus-4-6" }, "createdAt": "2026-03-05T10:00:00.000Z", "updatedAt": "2026-04-15T09:15:00.000Z"}Fields
Section titled “Fields”| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Workspace ID. Opaque ws_<16-hex>, matching the directory name; a directory with any other form is skipped. Name-independent — never parse it. |
name |
string |
Yes | Human-readable workspace name shown in the UI. Freely editable; changing it does not change the id or on-disk path. |
about |
string | null |
No | Short human-readable description, set in the workspace settings UI. Surfaced on the org “About” view and the workspace directory. Defaults to null. |
members |
array |
Yes | Users with access. Each entry is { userId, role } where role is "admin" or "member". |
connectors |
array |
Yes | Remote MCP connectors installed in this workspace. See Connector Configuration. |
connectorsAllowList |
string[] |
No | Per-workspace catalog allow-list. When set, only catalog entries whose id is in this array appear on the workspace’s Connectors page. When unset, the full loaded catalog is visible. Filters the catalog UI only — already-installed connectors keep running regardless. |
oauthOperatorApps |
object |
No | Per-workspace OAuth app config for auth: static catalog entries. Keyed by catalog id (reverse-DNS ServerDetail.name, e.g. "io.asana/mcp"); each value carries the public client_id plus an audit trail. The matching client_secret lives in the workspace credential store, not here. See Connectors Catalog → Where the secrets live. |
notifications |
object |
No | Notification source ceilings and delivery routes. notifications.sources maps a connector’s server name to { "maxLevel": "info" | "attention" | "urgent" } — the highest level that source’s items may reach a route at, defaulting to info for a source nobody has raised. notifications.routes is an array of { id, createdBy, match, deliver }. Written only by a workspace admin through Settings → Notifications; see Notifications. |
skillDirs |
string[] |
No | Directories scanned for skill markdown files, in addition to the built-in skills. |
models |
object |
No | Per-workspace model slot overrides. Partial — missing slots inherit from nimblebrain.json. |
createdAt |
string (ISO 8601) |
Yes | Timestamp, maintained by the runtime. |
updatedAt |
string (ISO 8601) |
Yes | Timestamp, maintained by the runtime. |
Notification ceilings and routes
Section titled “Notification ceilings and routes”notifications holds the operator half of the notifications contract: which of the workspace’s connectors may report how loudly, and where a matching item goes. Both are edited under Settings → Notifications — see Notifications for what each field means to an operator.
{ "notifications": { "sources": { "acme": { "maxLevel": "attention" } }, "cursors": { "acme": "eyJlIjoiZXBfMDFqOCIsImQiOjQyfQ" }, "routes": [ { "id": "rt_9f3c1a0b7d24", "createdBy": "usr_alex01", "match": { "source": "acme", "name": "domain.*", "level": "attention" }, "deliver": [ { "kind": "tool", "tool": "slack__send_message", "input": { "channel": "alerts", "text": "{{title}}\n{{inbox.url}}" } } ] } ] }}createdBy is the identity a route dispatches under — its workspace membership, its connector grants, its audit trail. The runtime stamps it from whoever saved the route; it is refused if a caller supplies it, and there is no field for delivering as anybody else. A route whose author is no longer a member is skipped rather than dispatched as nobody.
The block also carries a third key, cursors, which is not operator configuration: it is where the outbox poll recorded how far it has read in each connector’s outbox, one opaque server-issued token per connector. Nothing edits it by hand, and the settings surface neither shows nor touches it. Deleting an entry is safe but pointless — the next read starts from “now” rather than replaying, and uninstalling a connector removes its entry for you.
Workspace isolation
Section titled “Workspace isolation”Connectors, tool registries, and conversation data are scoped to a workspace. Every tool handler resolves its workspace via runtime.requireWorkspaceId() before touching data.
Two workspaces that install the same server hold independent connections and independent credentials. Each gets its own host-side data directory at <workDir>/workspaces/<wsId>/data/<serverName>/, so nothing crosses the workspace boundary. Sidebar placements, the overview’s facet counts, and the app list are filtered per workspace.
Model slot overrides
Section titled “Model slot overrides”Any workspace can override individual model slots. Slots you don’t specify fall through to the instance-level models in nimblebrain.json:
{ "models": { "default": "anthropic:claude-opus-4-6" }}Here the workspace uses Opus for default but still uses the instance default for fast.
Workspace persona
Section titled “Workspace persona”To give a workspace a distinct persona or domain focus, write it as workspace instructions (This Workspace → General). An identity key in workspace.json is ignored.
Related
Section titled “Related”nimblebrain.json— instance-level config- Connector Configuration — connector entry fields
- Features — feature flags (set instance-wide, not per-workspace)