Skip to Content
Agent computeREST: workspaces & regions
Raw

Aetherfy workspaces and regions API

Base URL https://agents.aetherfy.com/api/v1, bearer authentication, and the shared error envelope are described on the Agent REST API index.

A workspace groups agents and collections and gives them a shared region set and shared secrets. For the concept, see Workspaces.

Creating an Aetherfy workspace

POST /api/v1/workspaces → 201

curl -s -X POST https://agents.aetherfy.com/api/v1/workspaces \ -H "Authorization: Bearer $AETHERFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"research","description":"Research agents","regions":["iad","fra"]}'
FieldTypeNotes
namestringRequired. 3–63 characters, unique per account
descriptionstring | nullOptional
regionsstring[] | nullOptional. Omit to inherit your account’s default placement

Omitting regions is the normal case: Aetherfy resolves the workspace to your account’s default placement rather than leaving it unset. Supplying an empty array is a different thing and is rejected — a workspace with no regions could hold nothing.

The region set must fit your plan’s maximum. On Free and Starter that maximum is one, so a multi-region workspace requires the Performance plan — see Regions and replication.

{ "id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3", "name": "research", "description": "Research agents", "regions": ["iad", "fra"], "agent_count": 0, "created_at": "2026-08-19T09:30:00Z", "updated_at": "2026-08-19T09:30:00Z" }
CodeHTTPMeaning
WORKSPACE_NAME_TAKEN409
WORKSPACE_LIMIT_EXCEEDED400Your plan’s workspace count is reached
WORKSPACE_REGIONS_EMPTY422regions was an empty array. Omit it instead
INVALID_WORKSPACE_REGIONS400A region code is unknown, or there are more than your plan allows
USER_NOT_FOUND404

Reading and updating an Aetherfy workspace

GET /api/v1/workspaces → 200, an array. GET /api/v1/workspaces/{workspace} → 200, one workspace. PATCH /api/v1/workspaces/{workspace} → 200.

The path parameter is the workspace name, not its UUID.

curl -s -X PATCH https://agents.aetherfy.com/api/v1/workspaces/research \ -H "Authorization: Bearer $AETHERFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"description":"Research agents, 2026"}'

PATCH changes description only in practice. A workspace name is immutable: you may send name, but it has to equal the name already in the URL, and anything else is refused with 400 WORKSPACE_NAME_IMMUTABLE. To rename, create a new workspace and move resources into it.

Regions are not changed here either — they have their own route below.

Listing the agents in an Aetherfy workspace

GET /api/v1/workspaces/{workspace}/agents → 200

[ { "id": "6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411", "name": "reporter", "agent_type": "job", "status": "running", "deployed": true, "url": null, "is_degraded": false, "regions_total": 2, "regions_ready": 2, "degraded_reason": null, "created_at": "2026-08-19T09:04:00Z", "updated_at": "2026-08-19T09:06:02Z" }, { "id": "9d3f8a1c-77b2-4e60-8a15-c4e9f2d61b03", "name": "api", "agent_type": "service", "status": "running", "deployed": true, "url": "https://api-m4t7q3.aetherfy.dev", "is_degraded": false, "regions_total": 2, "regions_ready": 2, "degraded_reason": null, "created_at": "2026-08-19T09:04:00Z", "updated_at": "2026-08-19T09:06:02Z" } ]

Both agents are deployed; only one has a url. A service listens for requests and therefore has an address. A job runs to completion and exits — its image is not a web server, so there is nowhere to send a request and url is null. Read deployed to ask whether an agent is up; read url to ask where to reach it. They are different questions and were once one field.

A summary shape, not the full agent object — read GET /agents/{agent} for that.

deployed and url answer two different questions, and the example above is the case that shows why. deployed is whether the agent holds a running app — it is what the plan’s agent quota counts, and what archive, pause and run-now are offered on. url is where to send a request, and this agent has none: a job runs once and exits behind no HTTP server. Read deployed to tell whether an agent is running; read url only when you want to call one.

serves_websocket is a third read-only field, and it is derived rather than declared: Aetherfy reads what your deployed app actually serves — the live route table on the Python runtimes, the live upgrade handlers on node and bun — and reports it back. You never set it.

It has three values, and the third is the one worth knowing. true and false mean the agent answered and does or does not serve WebSocket routes. null means Aetherfy has no usable answer yet, which is not the same as false — a newly created agent, or one that has been asleep since it deployed, has told us nothing. Branch on === true rather than on truthiness if the difference matters to you.

Note that it says what an agent ADDITIONALLY accepts. Every service agent serves HTTP, because Aetherfy health-checks GET /health on all of them, so serves_websocket: true means HTTP and WebSocket rather than one instead of the other.

Deleting an Aetherfy workspace

DELETE /api/v1/workspaces/{workspace} → 200

{"status": "deleted", "secrets_deleted": 3}

Deletion is refused while the workspace still holds anything, and the response tells you which kind of thing is blocking it. Move or delete those first.

secrets_deleted reports how many workspace secrets went with it — they are the one thing deletion does remove, since nothing else can reach them.

CodeHTTPMeaning
WORKSPACE_HAS_AGENTS409Live agents remain: move or delete them first. agent_count is the number of live agents
WORKSPACE_AGENTS_STILL_DELETING409Only agents still being deleted remain: retry once their deletion finishes. agent_count is the number being deleted
WORKSPACE_HAS_COLLECTIONS409Move or delete its collections first

Changing an Aetherfy workspace’s regions

PATCH /api/v1/workspaces/{workspace}/regions → 202

curl -s -X PATCH https://agents.aetherfy.com/api/v1/workspaces/research/regions \ -H "Authorization: Bearer $AETHERFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"regions":["iad","fra","sin"]}'

regions is the desired final set, not a delta. Aetherfy computes what to add and remove.

{ "operation_id": "f2a7c390-4e81-4b25-9d06-8c1f5e0b7a43", "from_regions": ["iad", "fra"], "to_regions": ["iad", "fra", "sin"], "regions_to_add": ["sin"], "regions_to_remove": [], "workspace_name": "research" }

This is asynchronous. The 202 hands you an operation_id to poll rather than blocking until every collection has replicated — see below.

A no-op answers 200, not 202, and has no operation_id. If the set you send already equals the current one, Aetherfy enqueues nothing and returns a different body:

{"status": "no_op", "scope": "workspace", "current_regions": ["iad", "fra"], "workspace_name": "research"}

The comparison is by set, so reordering the same regions is also a no-op. Branch on the status code — or on the presence of operation_id — before trying to poll, because polling a no-op has nothing to poll.

The change affects the workspace’s collections and agents together. Widening the set makes a region available; getting a running agent into it still requires a redeploy.

Adding a region does not copy anything into it: the workspace’s collections keep the regions they have, and its agents keep the regions they were deployed to. So a widen adds no storage and is never refused for space; a collection’s own regions change, which does add replicas, is where the storage quota applies (changing a collection’s regions). A workspace’s regions are the ceiling for what is in it, not a placement.

CodeHTTPMeaning
WORKSPACE_NOT_FOUND404
USER_NOT_FOUND404
VALIDATION_VIOLATIONS400One or more checks failed — an unknown region (INVALID_REGION), more regions than your plan allows, or a component not ready. Iterate detail.violations
REGION_OP_IN_FLIGHT409A region change is already running for this workspace
WORKSPACE_NARROW_BLOCKED_BY_RESOURCES422Narrowing would strand resources that still use a region you are removing
STARTER_REGION_CONSISTENCY422On a single-region plan, the region does not match the account home region

WORKSPACE_NARROW_BLOCKED_BY_RESOURCES refuses the change rather than shrinking anything, so a narrow never deletes data as a side effect. Its body names every blocker at once — regions_to_remove, plus an agent_conflicts array and a collection_conflicts array, each entry giving the resource name and the regions it currently holds. A collection conflict also carries the collection’s id:

{"detail": {"code": "WORKSPACE_NARROW_BLOCKED_BY_RESOURCES", "message": "This region change removes ['sin'], but 2 resource(s) still use a region being removed. ...", "regions_to_remove": ["sin"], "agent_conflicts": [{"name": "pdf-parser", "deployment_regions": ["iad", "sin"]}], "collection_conflicts": [{"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "name": "documents", "regions": ["iad", "sin"]}]}}

A collection that is being moved into the workspace counts as already in it.

Clear each conflict first — redeploy the listed agents without the region, and PATCH each listed collection to drop it — then retry the narrow. Note this one is a top-level detail.code, not a pre-flight violation entry: it is checked after the validations below pass, so you will not see it in a detail.violations array.

Pre-flight violations on an Aetherfy region change

Before enqueueing, Aetherfy checks the whole change and reports every problem at once rather than failing on the first. That answer is 400 with the top-level code VALIDATION_VIOLATIONS and a detail.violations array — each entry carrying its own code. Read the array, not the top-level code, or you will see one failure and fix one of four.

Violation codeMeaning
INVALID_REGIONNot a region Aetherfy runs in. One entry per bad region, each naming it
PLAN_REGION_LIMITMore regions than your plan allows for this scope. Names the limit and what you asked for
REGION_UNAVAILABLEThe region is temporarily closed to new placements. Transient — retry later
COMPONENT_NOT_READYA collection or agent in the workspace is mid-transition. Names it and its state; wait for a stable one
AGENT_REGION_STRANDEDAn agent has a live deployment in a region this change would remove. Lists deployment_regions, target_regions and stranded_regions — redeploy the agent narrower, or widen the target set

INVALID_REGION is the one code here that also appears at the top level, on PATCH /users/me/home-region — same code, different position in the envelope, because that route validates a single region rather than a set.

Moving an Aetherfy collection between workspaces

PATCH /api/v1/collections/{collection_id} → 200 or 202

curl -s -X PATCH https://agents.aetherfy.com/api/v1/collections/$COLLECTION_ID \ -H "Authorization: Bearer $AETHERFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id":"d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3"}'
FieldTypeNotes
workspace_iduuid | nullTarget workspace, or null to move the collection out of any workspace
regionsstring[] | nullAn explicit region set for the collection

The path parameter is the collection’s UUID, not its name — this is a control-plane route, unlike the name-addressed collection routes on the vector API.

Send workspace_id or regions, not both: combining them is refused, because the two are checked against each other. regions changes where the collection’s data is; workspace_id changes where the collection is reachable from.

A collection belongs to one workspace, or to none, and that decides where it is reachable from: requests through that workspace reach it, requests through another workspace or outside any do not. It does not decide what the collection may be called. Collection names are unique per account, so a name taken in one workspace is taken everywhere, and moving a collection keeps its name taken in the workspace it left.

A move changes only the workspace. The collection keeps its regions, and its data stays where it is: nothing is copied or deleted, and your storage use does not change. So the target must allow every region the collection is placed in — the target workspace’s regions, or outside any workspace your plan’s region rules. A collection placed anywhere else is refused with COLLECTION_MOVE_OUTSIDE_TARGET_REGIONS, naming the regions that do not fit in regions_outside_target. Change the collection’s regions first, or add those regions to the target workspace, then move it.

A move answers 202 with an operation_id; asking for the workspace it is already in answers 200 with "no_op": true. When the operation succeeds, the collection answers through its new workspace and no longer through the old one.

{ "operation_id": "8c3e5a19-7f20-4d6b-a1e4-0b97c2d5f816", "collection_id": "3a9f6e2c-51d8-4b07-9c14-e8d2f0a7b635", "source_workspace_id": null, "target_workspace_id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3", "regions": ["iad"] }

Aliases do not survive the move. Aetherfy deletes the collection’s aliases in the source scope and does not recreate them in the target. The completed operation lists what went in deleted_aliases so you can recreate them deliberately. Each one is named as you name it: the alias without its workspace, as the collection’s alias list answers, and the collection by its own name.

"deleted_aliases": [{"alias_name": "articles-live", "collection_name": "articles"}]

The memory SDK’s conversation collection cannot be moved. A MemoryClient keeps its conversations in one collection per workspace, __threads__<workspace> (or __threads__ outside any workspace), and only ever reads its own workspace’s. Moving one is refused with COLLECTION_SDK_MANAGED; use a MemoryClient in the target workspace, which keeps its own.

CodeHTTPMeaning
COLLECTION_NOT_FOUND404
TARGET_WORKSPACE_NOT_FOUND404
USER_NOT_FOUND404
COLLECTION_MOVE_IN_FLIGHT409A move or a regions change is already running for this collection
REGION_OP_IN_FLIGHT409A region change is running on the target workspace
COLLECTION_REGIONS_WORKSPACE_COMBINED400Both workspace_id and regions were sent
COLLECTION_REGIONS_EMPTY422regions was an empty array
COLLECTION_REGIONS_NOT_IN_SCOPE422The requested regions are outside the collection’s workspace, or more than your plan allows outside a workspace
COLLECTION_MOVE_OUTSIDE_TARGET_REGIONS422The collection is placed in a region the target does not allow. Names regions_outside_target; change the collection’s regions or widen the target workspace first
COLLECTION_SDK_MANAGED422The collection is the memory SDK’s conversation collection, which cannot be moved
STORAGE_LIMIT_EXCEEDED422The regions you added would put the collection’s replicas over your storage quota
REGION_CHANGE_COUNT_LIMIT_EXCEEDED422The change copies data, and your plan’s region changes that copy data in 30 days are used up. Names limit, used, needed and frees_at
REGION_CHANGE_SIZE_LIMIT_EXCEEDED422The change would copy more than your plan has left to copy between regions in 30 days. Names limit_bytes, used_bytes, needed_bytes and frees_at
STARTER_REGION_CONSISTENCY422On a single-region plan, the region does not match the account home region
VALIDATION_VIOLATIONS400Several rules failed at once. Iterate detail.violations
COLLECTION_MOVE_ENQUEUE_FAILED500The change could not be queued. Retry

VALIDATION_VIOLATIONS is the accumulating envelope: rather than failing on the first problem, Aetherfy reports every rule the request broke in one violations array, each entry carrying its own code. Read that array rather than the top-level message.

Changing an Aetherfy collection’s regions

PATCH /api/v1/collections/{collection_id} with regions changes where the collection’s data is. A region you add gets a copy of the collection’s data; a region you remove has its copy deleted. It answers 202 with an operation_id, and the collection keeps serving from its current regions until the operation finishes.

A change that copies data is limited per account, over a rolling 30 days. A change copies data when it adds a region. Two limits apply, and a change past either is refused before anything is copied:

LimitPerformanceEnterprise
Region changes that copy data, per 30 days10unlimited
Data copied between regions, per 30 days400 GB — your storage, times your regions minus oneunlimited

A change that only removes regions is not counted, and neither is a move, which copies nothing. Free and Starter allow none: they are single-region, so no change of theirs can copy data, and their own region rule (STARTER_REGION_CONSISTENCY) refuses an added region first. Every change that copied data in the last 30 days counts, whether it finished or failed. Its size is the collection’s stored bytes times the regions it added, as they were when the change was accepted.

A refusal names the limit, what the last 30 days have used, what this change needs, and frees_at — when the oldest change that has to leave the window does. A change too large for the whole size limit has frees_at: null: no window allows it, so add fewer regions at a time. Your storage quota (STORAGE_LIMIT_EXCEEDED) is checked first.

Previewing a regions change

POST /api/v1/collections/{collection_id}/regions/preview → 200

Asks what the change would do, without doing it. The answer comes from the same code the change runs, so the change does what its preview says. Send the same regions:

curl -s -X POST https://agents.aetherfy.com/api/v1/collections/$COLLECTION_ID/regions/preview \ -H "Authorization: Bearer $AETHERFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"regions":["iad","fra"]}'
{ "collection_id": "3a9f6e2c-51d8-4b07-9c14-e8d2f0a7b635", "from_regions": ["iad"], "to_regions": ["iad", "fra"], "regions_to_add": ["fra"], "regions_to_remove": [], "no_op": false, "copy_bytes": 3221225472, "data_deleted_from": [], "limits": { "counted": true, "window_days": 30, "count": { "limit": 10, "used": 3, "this_change": 4 }, "size": { "limit_bytes": 429496729600, "used_bytes": 53687091200, "left_after_bytes": 372588412928 } }, "refusal": null }

refusal is null, or the answer the change would get: its status, code, message and the refusal’s own fields. The preview answers 200 either way; it is 404 only for a collection that is not yours, and 422 COLLECTION_REGIONS_EMPTY for an empty list. limits is null for a change that would be refused before the limits are read, and for a no-op. An accepted change’s 202 carries the same object as plan.

Polling an Aetherfy long-running operation

GET /api/v1/operations/{operation_id} → 200

Both the region change and the collection move above return an operation_id. This is where you follow it.

curl -s https://agents.aetherfy.com/api/v1/operations/$OPERATION_ID \ -H "Authorization: Bearer $AETHERFY_API_KEY"
{ "operation_id": "f2a7c390-4e81-4b25-9d06-8c1f5e0b7a43", "scope": "workspace", "target_id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3", "status": "succeeded", "regions_to_add": ["sin"], "regions_to_remove": [], "started_at": "2026-08-19T09:31:02Z", "completed_at": "2026-08-19T09:34:47Z", "failures": null, "deleted_aliases": null }
statusMeaning
pendingQueued, not started
in_progressRunning, or briefly between retries
succeededTerminal, done
failedTerminal. failures carries the reasons

in_progress deliberately covers the short window after a retryable sub-step failed and before it is retried, so a transient blip does not surface as failed. Only failed is a terminal failure.

failures is null unless status is failed. Then it holds one object per step that did not complete, and every object has a message:

FieldPresentMeaning
messageAlwaysWhat did not complete, in a sentence
sub_stepWhen the step was recordedThe step’s id within the operation
typeWhen the step was recordedThe kind of step, for example backfill
regionWhen the step was recordedThe region the step ran in
attemptsWhen the step was recordedHow many times the step was tried
collection_idCollection stepsThe collection the step acted on
alias_name / collection_nameAlias stepsThe alias, and the collection it points to, as you name them: the alias without its workspace

An operation that failed before it recorded which step has a single entry with message alone.

"failures": [ { "sub_step": "backfill-abc-eu-central-1", "type": "backfill", "collection_id": "abc", "region": "eu-central-1", "attempts": 5, "message": "The backfill step in eu-central-1 did not complete." } ]

deleted_aliases is populated on a collection move and null on a region change.

Returns 404 OPERATION_NOT_FOUND for an unknown id or one belonging to another account.

Last updated on