Skip to content

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.

{
"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"
}
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.

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.

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.

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.

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.