Agent API Workspaces
Status: live Agent API resource.
Workspaces are the mandatory containers for execution and content resources. This member-plane resource exposes the collection, owner management, and the strict serial lifecycle. There is no default Workspace: the empty list is a normal first answer, and an Agent program creates its own dedicated private Workspace when none fits.
Management commands exist so a Human owner credential can manage the aggregate over the member plane. An Agent principal that can browse a row but invokes an owner-only command receives an honest 403 not_workspace_owner.
Routes
GET /agent_api/v1/workspaces
GET /agent_api/v1/workspaces?state=archived
GET /agent_api/v1/workspaces/{workspace_id}
POST /agent_api/v1/workspaces
PATCH /agent_api/v1/workspaces/{workspace_id}
PUT /agent_api/v1/workspaces/{workspace_id}/access_mode
GET /agent_api/v1/workspaces/{workspace_id}/messaging
PUT /agent_api/v1/workspaces/{workspace_id}/messaging
PUT /agent_api/v1/workspaces/{workspace_id}/tool_provider_overrides
GET /agent_api/v1/workspaces/{workspace_id}/principals
GET /agent_api/v1/workspaces/{workspace_id}/prompt_documents
GET /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}
PUT /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}
DELETE /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}
GET /agent_api/v1/workspaces/{workspace_id}/memory
POST /agent_api/v1/workspaces/{workspace_id}/memory/show
POST /agent_api/v1/workspaces/{workspace_id}/memory
POST /agent_api/v1/workspaces/{workspace_id}/memory/grep
POST /agent_api/v1/workspaces/{workspace_id}/memory/edit
POST /agent_api/v1/workspaces/{workspace_id}/memory/delete
POST /agent_api/v1/workspaces/{workspace_id}/ownership_transfer
POST /agent_api/v1/workspaces/{workspace_id}/archival
POST /agent_api/v1/workspaces/{workspace_id}/restoration
DELETE /agent_api/v1/workspaces/{workspace_id}?lock_version=...- The default state scope is
live(active | restoring);state=archivedis the recycle bin (archiving | archived); any other value is400 parameter_invalid. Tombstones are absent from every surface. - Dedication visibility is applied after ordinary owner/access-mode/steward access. For an Agent, omitting
dedicated_to_current_agentor passingfalsereturns only undedicated (agent_identifier IS NULL) Workspaces;truereturns only Workspaces matching that Agent's Agent identifier. For a Human, omission orfalseretains every accessible Workspace, whiletruereturns an empty collection. Multiple matches are legal. - Dedication grants no access. The exact identifier is resolved server-side and never crosses the wire; direct reads keep their ordinary access semantics, while a dedicated row remains the final mismatched-Agent write fence.
GET …/workspaces/{id}/principals— THE PRINCIPALS LISTING: who may be named on a conversation's access carrier — every member User of the account with access to this workspace (the one access relation, answered per row: a Human by owner-or-account-wide coverage, an agent through its live steward's), of either kind, ordered by display name, as{ principals: [{ public_id, handle, kind, display_name, agent_identifier, steward_public_id }] }. Every key is present on every row — a Human'sagent_identifierandsteward_public_idare null, never absent.handleis the member's NAME — unique per account,[a-z0-9][a-z0-9_-]{1,31}— accepted by the access carrier and by agent selection (agentonspawn/send, or a turn'sanswering_user_public_id), written@handleor bare. The tools'toparameter instead names a conversation by public id or child label. The kernel assigns an agent's handle, and its steward may rename it. The system user is never a member and never listed. No pagination: the set is an account's members with access, small by construction (the executors listing's shape). This is the member plane's ONE user listing: an agent learns its peers' ids here and its own steward's off its own row;rho conversation participants(rho-dev) and the agent-selection clients read it. Absence of access is absence: a workspace the caller cannot browse is 404.- Lists paginate by keyset: ordering is over the list's own key columns, ASCENDING by default and
order=descfor the other direction — over those key columns, so on a name-keyed listdescis reverse alphabetical rather than newest. The cursor CARRIES its direction: a page taken one way and continued the other would walk back over rows the caller already has, so that combination is refusedparameter_invalidrather than served.afteris an opaque cursor,limitdefaults to 25 and accepts 1–100. Non-integer values, values below 1, and values above 100 return400 parameter_invalid. lock_versionis required on every mutating command except create: in the JSON body for PATCH/PUT/POST commands, as a required query parameter on DELETE. Missing is400 parameter_missing; malformed, negative, or beyond the PostgreSQL integer range is400 parameter_invalid.
Create and idempotency
POST /agent_api/v1/workspaces requires a client-minted Idempotency-Key header (1–255 bytes; missing or empty is 400 idempotency_key_required). The accepted request is digested after allowlisting; for 24 hours the same key with the same digest replays the stored 201 verbatim, while a different digest is 409 idempotency_envelope_mismatch. Idempotency-Replayed is false for a new creation and true for a replay. Replay re-resolves the created Workspace through the caller's current scope — access loss or a tombstone is 404, never a leaked stored response.
Body (workspace): name (required), access_mode (optional; Human principals only), and metadata (optional JSON object, 2 KiB canonical-JSON bound). An Agent create always forces private and derives the dedication tag from the authenticated Agent; a Human create stores a null tag. dedicated and raw agent_identifier are outside every create allowlist and ignored like any unknown field. Dedication is creator-derived and frozen at creation; there is no input, mutation field, or command.
Update
PATCH /agent_api/v1/workspaces/{workspace_id} requires lock_version and at least one of name or metadata. When present, metadata must be a non-null JSON object under the same 2 KiB canonical-JSON bound as create. A non-object value returns 422 validation_failed; an oversized object returns 413 content_too_large.
Owner management
Changing access mode uses this exact envelope:
PUT /agent_api/v1/workspaces/{workspace_id}/access_mode
Content-Type: application/json
{"access_mode":{"access_mode":"account_wide","lock_version":0}}Ownership transfer names an active Human in the same Account:
POST /agent_api/v1/workspaces/{workspace_id}/ownership_transfer
Content-Type: application/json
{"ownership_transfer":{"target_user_public_id":"019f0000-0000-7000-8000-000000000401","lock_version":0}}Archival and restoration use the same command envelope:
POST /agent_api/v1/workspaces/{workspace_id}/archival
POST /agent_api/v1/workspaces/{workspace_id}/restoration
Content-Type: application/json
{"command":{"lock_version":0}}Lifecycle acceptance may answer with archiving or restoring. Wait for the stable archived or active state and refetch its current lock_version before issuing the next lifecycle command.
Read messaging
The messaging policy sets who may contact each recipient Agent in this Workspace through the kernel's send and spawn mechanisms. Each Agent is Public or Private. This setting is independent of Workspace and Conversation data access.
Endpoint
GET /agent_api/v1/workspaces/{workspace_id}/messaging
Request
No request body. A member with data access to the Workspace may read its policy.
Response
{
"messaging": {
"agents": [
{
"agent_public_id": "019f0000-0000-7000-8000-000000000302",
"mode": "private",
"allowed_sender_public_ids": ["019f0000-0000-7000-8000-000000000301"]
}
],
"lock_version": 7
}
}Each entry names the recipient in agent_public_id. For A to contact B, Nexus reads B's entry; A's mode controls who may contact A.
| Mode | Accepted senders |
|---|---|
public | Every otherwise eligible Agent. This is the default when the recipient has no entry. |
private | Agents in allowed_sender_public_ids, plus the recipient itself. |
Self-contact is always allowed. A Private Agent with an empty sender list accepts only itself. An explicit Public entry keeps its sender list but ignores that list for admission, so switching back to Private can reuse it. An empty agents list removes every saved setting and restores Public for every Agent. Each paired or named Agent has its own public UUID, even when one program's credential serves several bots. Permission from A to B does not imply permission from B to A.
lock_version belongs to the Workspace. This resource does not have a separate version or add fields to the Workspace Basic or Full projections.
Status codes
200 returns the policy. Authentication and Workspace visibility use the Agent API common contract and this page's status mapping.
Replace messaging
Endpoint
PUT /agent_api/v1/workspaces/{workspace_id}/messaging
Request
The body has the same messaging envelope as the read response. agents and the observed Workspace lock_version are required. The list replaces the entire policy; omitting an Agent removes its saved setting and sender list, returning that Agent to Public. Only the active Human owner of the Workspace may replace it, and the Workspace must be active. Account administrator status or an Agent's steward relationship does not grant this management authority.
Each entry requires agent_public_id and mode (public or private); allowed_sender_public_ids defaults to an empty list when omitted. The IDs name existing Agent members of this Account, including named definitions. Each recipient appears at most once; allowed sender IDs form a set. Self-contact remains allowed whether or not the recipient appears in its own sender list. A Public entry preserves the supplied sender list without using it to restrict contact. An existing Agent's removal, suspended steward or lack of current Workspace access does not erase or invalidate the stored relationship. Ordinary eligibility and access checks still decide whether that Agent can act here. The stored policy document has a 64 KiB canonical-JSON bound.
Response
200 returns the policy in the read response shape, with the current Workspace lock_version. Refetch and reconcile a stale edit before submitting another replacement; clients must not silently overwrite a newer policy.
Status codes
| HTTP | Code | Meaning |
|---|---|---|
400 | parameter_missing, parameter_invalid | The envelope, agents list or Workspace version is missing or malformed. |
403 | not_workspace_owner | The caller is not the active Human Workspace owner. |
404 | not_found | The Workspace is absent from the caller's scope. |
409 | workspace_not_active | The Workspace is not active. |
409 | stale_object | Another Workspace write changed the observed version. |
413 | content_too_large | The policy exceeds its 64 KiB bound. |
422 | validation_failed | The policy is invalid, including a missing or unknown mode, a repeated recipient or an ID that is not an Agent of this Account. |
Admission
The recipient's setting judges a new send or spawn from the calling Run's answering Agent. The Run's creator, credential holder and input origin do not select the sender. An omitted agent still resolves a recipient; an unnamed steer targets the current reply's answerer. Queue, delayed delivery and steer apply the same policy. A refused spawn creates no child Conversation. The tool result reports is_error: true and names messaging_not_allowed; this is not a new refusal on the ordinary member input API.
A later policy edit does not cancel an accepted input or recheck its messaging permission when it drains. Existing input eligibility checks still apply. Kernel result mail and child replies do not require reverse permission. status, cancel, history reads, and ordinary Human/member-plane inputs retain their existing authorization. The policy grants no Conversation access and does not conceal data that a member may already read.
Tool provider overrides
A workspace may opt a kernel tool FAMILY into a tool provider: the provider then serves that family's verbs in the kernel's place, for this workspace alone.
PUT /agent_api/v1/workspaces/{workspace_id}/tool_provider_overrides
Content-Type: application/json
{"tool_provider_overrides":{"overrides":{"nexus.memory":"<provider public_id>"},"lock_version":0}}overrides is a WHOLE REPLACEMENT of the map — namespace → the public id of a tool provider — and is required: {} clears every override (an absent key is 400 parameter_missing, never "clear"); a value that is not a string is 400 parameter_invalid. lock_version is the workspace's, CASed exactly as the metadata PATCH CASes it, so a concurrent PATCH answers 409 stale_object. The overridable namespaces are the live kernel namespaces outside the reserved set — today exactly nexus.memory (memory_read, memory_write, memory_edit, memory_ls, memory_grep, memory_delete); the pack lists them as overridable_namespaces. nexus.skill is live and announceable but never overridable — a skill load is routed per call by its name's source (Executor, "Where a tool call goes"), so {"nexus.skill": …} is 422 validation_failed (the model's namespace_not_overridable error under it, as check 1 below says).
WHO may set it: a caller with WRITE standing under the dedication fence — any member with data access, an agent whose dedication the workspace is; never a fenced agent (403 workspace_agent_identifier_mismatch), never outside active (409 workspace_not_active). Ownership is not required: an agent with write standing may opt its own workspace in.
Three checks run before any lock, in this order:
422 reserved_namespace— the key names a reserved kernel namespace (nexus.graph,nexus.human,nexus.conversation,nexus.tools): no provider may serve one. The same code the announcement door answers. A key that is neither reserved nor overridable is422 validation_failed, naming it.422 provider_not_eligible— the value is not a live tool provider of this account, or its scope would darken other members: addressing requires the provider to be eligible for the run's principal, so the PUT admits a provider only when it isaccount_wide, or the workspace isprivateand its owner is the provider's manager. A workspace later widened toaccount_widekeeps auser_privateoverride; the new members' calls then failtool_not_served, honest on the transcript and visible on the read below.422 provider_incomplete— the provider does not announce EVERY live wire name of the namespace (names only). A provider takingmemory_readwhile the kernel keptmemory_writewould be two memories under one family.
The kernel's description bytes are the contract. memory_read's text promises scope semantics ("shared with everyone who can open this workspace"), and opting in makes that text the PROVIDER's to honour: the kernel stamps every routed row with scope: {bindings: [...]} beside tool_input, which passes through untouched. Each binding names its logical root, database scope, public anchor UUID, and read or read_write access. The provider must honor that resolved binding set, including aliases and unavailable roots; it must not infer three default roots from the host IDs. See Executor for the wire shape.
Drift is level-triggered. A provider that later drops a verb fails THAT verb's calls tool_not_served; a provider that is revoked fails them all; the read still names it. There is no kernel fallback: a tool name resolves to exactly one providing authority.
What is silenced while a namespace is overridden (nexus.memory): the six verbs ride the inbox to the provider; the assembly block renders NO memory for this workspace's conversations; the conversation memory routes (…/conversations/{id}/memory, Conversation memory) refuse 409 memory_overridden naming the provider for reads and writes; the kernel's rows persist untouched, so clearing the override restores them. The person's own door (profile/memory) is not a workspace's and keeps serving user/. In an overridden Workspace, a turn with no memory tools cannot read that provider's memory: the kernel's injected block is absent, and the provider's memory is reachable only through its tool verbs. A direct_reply input may have tools when its answering Agent declares them; the input kind does not determine tool availability.
The Full projection renders the map with the provider's display_name and assignment_scope so a person can see why; a reaped provider renders null for both beside its id (the id is a snapshot, not a reference). Setting it is not narrated: the read, lock_version and updated_at are the record. No rho verb sets it — the SDK's workspace.set_tool_provider_overrides is its client.
Prompt documents
A workspace holds ONE prompt-document slot, character — the room's identity and scenario — compiled by the default template into every assembled turn of every conversation in it, behind the agent's system_prompt and ahead of the person's persona and memory (Prompt assembly). The slot is the URL's member segment — one word, never a slash — so unlike memory every verb addresses it in the path, and the write is a PUT: a whole replacement, first or later write alike.
GET /agent_api/v1/workspaces/{workspace_id}/prompt_documents
GET /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}
PUT /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}
DELETE /agent_api/v1/workspaces/{workspace_id}/prompt_documents/{slot}{ "prompt_document": { "content": "You are the narrator of {{workspace}}, speaking with {{user}}.", "role": "system" } }{ "prompt_document": { "slot": "character", "role": "system", "bytesize": 63, "version": 2, "content": "…", "written_at": "2026-09-08T00:00:00Z" } }GET …/prompt_documents— every slot this workspace holds, as{slot, role, bytesize, version, written_at}(no content:GET …/{slot}is what loads text).PUT …/prompt_documents/{slot}—{prompt_document: {content, role?}}writes the document whole; 200 with the document whether first or later write,versioncounting the writes.roleis the block's role in the sealed list —system(the default),developeroruser. Omittingroleusessystemon every write, including replacement of a document with another role. A role other thansystembreaks the leading system run and stands as its own item, the author's choice. The content rides as written: the macros{{agent}} {{user}} {{workspace}} {{date}} {{conversation_kind}}are substituted at compile, and a word outside that registry is refused here, naming it.DELETE …/prompt_documents/{slot}— 204, and it is gone: no history. The sealed requests that compiled it keep the bytes they were sent.
WHO may write: WRITE standing under the dedication fence — any member with data access, an agent whose dedication the workspace is — never a fenced agent, which reads it and never writes it; never outside active. Reads take browse standing; an archived room still reads. The workspaces row is the lock. A write bumps no conversation's context_revision: the next turn of every conversation reads the slot live. Bound: 64 KiB.
| Status | Code | Description |
|---|---|---|
200 OK | — | The listing, the document, or the written document. |
204 No Content | — | Deleted. |
401 Unauthorized | unauthorized | The credential is missing, unusable, or not a member-plane credential. |
403 Forbidden | not_authorized | A write without write standing — a fenced agent, a member without data access. |
404 Not Found | not_found | The workspace is not visible to the caller. |
404 Not Found | prompt_document_not_found | The slot holds no document. |
409 Conflict | workspace_not_active | A write outside active. |
422 Unprocessable Content | prompt_slot_unavailable | The slot is not this door's: a workspace anchors character alone (system_prompt and persona live on profile/prompt_documents). |
422 Unprocessable Content | prompt_document_too_large | The content exceeds 64 KiB. |
422 Unprocessable Content | prompt_document_macro_unknown | A {{word}} outside the registry, named in the message. |
422 Unprocessable Content | prompt_document_invalid | No content, a non-string, a role outside system | developer | user. |
Memory
The workspace's own door to workspace/ memory: rows available to conversations whose effective bindings include this root, written and read here without selecting a conversation. It uses the profile door's shape on the workspace's anchor. user/, agent/, speaker/ and conversation/ are managed through their owning doors (Profile memory, Conversations) and are refused here with memory_scope_unavailable. The path rides the body on every verb that names one, the reads included.
GET /agent_api/v1/workspaces/{workspace_id}/memory
POST /agent_api/v1/workspaces/{workspace_id}/memory/show
POST /agent_api/v1/workspaces/{workspace_id}/memory
POST /agent_api/v1/workspaces/{workspace_id}/memory/deleteGET …/memory— everyworkspace/document of this room, as{path, public_id, lock_version, bytesize, description, written_at}(no content:showis what loads text). Only this workspace's rows are listed.POST …/memory/show—{memory: {path}}answers one document with itscontent.POST …/memory—{memory: {path, content, description?, expected_public_id, expected_lock_version}}writes one document whole. 201.descriptionis a skill row's (workspace/skills/<name>, Conversation memory).POST …/memory/grep—{memory: {pattern, path?, ignore_case?, limit?}}; 200{matches: [{path, line_number, text}], truncated}. Only this workspace's documents are searched, with the bounded regex semantics described in conversation memory.POST …/memory/edit—{memory: {path, old_text, new_text, expected_public_id, expected_lock_version}}; 200{memory: ...}. The expected version must match and the exact passage must occur once; a refusal changes nothing.POST …/memory/delete—{memory: {path, expected_public_id, expected_lock_version}}. 204, and it is gone.
Both expected fields are required: null/null creates only an absent path; a UUID/version pair from read or list replaces or deletes only the observed row. Missing or malformed conditions return 400 parameter_missing / parameter_invalid; stale, missing, or recreated rows return 409 stale_object without changes. See conditional writes for the shared create, delete, fork, and conflict contract. Never automatically retry an old calculation with a newly fetched pair.
WHO may write: WRITE standing on the workspace — data access under the dedication fence, in active; a browsable-but-not-writable caller (an archived room's reader, a fenced agent) reads and never writes (403 not_authorized). The workspaces row is the lock. A write bumps no conversation's context_revision: the next turn of every conversation in the room reads the rows live. Bounds and the 64-document cap are memory's.
WHILE THE WORKSPACE IS OVERRIDDEN (tool_provider_overrides above) every plain verb here is 409 memory_overridden naming the provider — the ONE guard the conversation door runs (MemoryOverrideGuard); a skills/ path passes it, a skill being the kernel's row and never the provider's. The person's own door keeps serving user/ meanwhile.
| Status | Code | Description |
|---|---|---|
200 OK | — | The listing or the document. |
201 Created | — | The written document. |
204 No Content | — | Deleted. |
400 Bad Request | parameter_missing, parameter_invalid | A required condition is missing or malformed. |
409 Conflict | stale_object | The selected row/version no longer matches, or an absent path was created. |
403 Forbidden | not_authorized | A write without write standing on the workspace. |
404 Not Found | not_found | The workspace is not visible to the caller. |
404 Not Found | memory_not_found | A read found no document at that path in this room. |
409 Conflict | memory_overridden | The workspace's memory is a provider's; the message names it. |
422 Unprocessable Content | memory_scope_unavailable | A user/, agent/, speaker/ or conversation/ path — those scopes have their own doors. |
422 Unprocessable Content | memory_path_invalid | A bare name, or a scope word outside conversation | workspace | user | agent | speaker. |
422 Unprocessable Content | memory_full, memory_document_too_large, memory_content_invalid, memory_description_invalid, skill_description_required, skill_name_invalid | Memory's own refusals, as on every door. |
Projections
Workspace Basic (lists): public_id, name, access_mode, state, dedicated, lock_version, archived_at, created_at, updated_at.
Workspace Full (singular responses) adds metadata, tool_provider_overrides {namespace → {provider_public_id, display_name, assignment_scope}} ({} when none), owner {public_id, display_name}, and creator {public_id, display_name, kind}.
dedicated is the only public dedication marker; it is boolean and the exact identifier never renders anywhere.
Status mapping
| Outcome | HTTP |
|---|---|
| created | 201, Full projection |
| updated / transferred / lifecycle accepted | 200, Full projection |
| repeat of an already-current state | 200, Full projection (state-based no-op) |
| transition in progress | 409 transition_in_progress |
| no effective access, tombstoned, unknown id | 404 not_found |
| reader without management authority | 403 not_workspace_owner |
| mismatched Agent write on a dedicated Workspace | 403 workspace_agent_identifier_mismatch |
management outside active | 409 workspace_not_active |
stale lock_version | 409 stale_object |
| ineligible or self transfer target | 422 target_not_eligible |
| unsupported access mode | 422 invalid_access_mode |
| override of a reserved kernel namespace | 422 reserved_namespace |
| override provider not live, not a tool provider, or out of the members' scope | 422 provider_not_eligible |
| override provider not announcing the whole namespace | 422 provider_incomplete |
| metadata over its bound | 413 content_too_large |
| other validation failures | 422 validation_failed |