Workspaces
Create, manage, and control development workspaces. Workspaces are interactive computing environments (e.g., Jupyter, VS Code, RStudio) provisioned within a project.
All endpoints require authentication via X-API-Key header and the appropriate scope.
Workspace Object
{
"_id": "ws_ghi789",
"name": "Training Environment",
"description": "GPU-enabled workspace for model training",
"projectId": "proj_abc123",
"branch": "main",
"status": "running",
"environment": {
"type": "jupyter",
"label": "Jupyter Lab",
"port": 8888
},
"resources": {
"cpu": "4",
"memory": "16GB",
"disk": "20GB",
"gpu": "1",
"gpuType": "nvidia-a10g",
"useSpot": true,
"spotFallback": true
},
"workspaceVolumeSize": "50GB",
"environmentVariables": { "EPOCHS": "10" },
"addons": [],
"dataSources": [],
"aiGateways": [],
"workflows": [],
"codingAssistants": ["claude-code"],
"codeSessionEnabled": false,
"cluster": {
"engine": "ray",
"coordinator": { "cpu": "2", "memory": "4GB" },
"worker": { "cpu": "2", "memory": "4GB" },
"workers": 2,
"status": "ready",
"connectAddress": "ray://<host>:10001",
"dashboardUrl": "https://.../dashboard"
},
"volumes": [
{
"volumeId": "vol_def001",
"name": "training-data",
"scope": "local",
"code": { "remote": "git@github.com:my-org/training.git", "branch": "main" },
"data": { "version": "latest" }
}
],
"organizationId": "org_xyz",
"owner": "user_456",
"url": "/api/workspace-proxy/ws_ghi789/lab",
"createdAt": "2025-01-16T11:00:00Z",
"updatedAt": "2025-02-01T08:00:00Z"
}
resources.diskis the container's ephemeral scratch;workspaceVolumeSizeis the persistent/workspacevolume.environment.typeis the IDE (jupyter,vscode,rstudio,custom) andenvironment.portthe port it serves on (for a Custom IDE, thecustomPortyou set); a Custom IDE also carries any declaredproxyHeaders.statusis one ofbuilding,stopped,deploying,starting,running,stopping,error.owneris the id of the user who created the workspace.urlis the path the workspace opens at on your instance.platformUpdatePendingistruewhile the running workspace was deployed by an earlier platform version;POST /workspaces/:id/restartapplies the update (it ends open notebooks, terminals and processes). Absent otherwise. See Platform Updates.volumeslists the code/data volumes the workspace mounts, as they mount.clusteris present only when a compute cluster is attached; once ready it exposes theconnectAddressanddashboardUrl. See Compute Clusters.
GET /api/v1/workspaces
List all workspaces accessible to the authenticated user.
Scope: workspaces:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | No | Search by name or description |
status | string | No | Filter by status: building, stopped, deploying, starting, running, stopping, error |
projectId | string | No | Filter by project ID |
limit | integer | No | Number of results to return (default: 50, max: 200) |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | Sort field, prefixed with - for descending (default: -createdAt) |
Response 200 OK
{
"data": [
{
"_id": "ws_ghi789",
"name": "Training Environment",
"description": "GPU-enabled workspace for model training",
"projectId": "proj_abc123",
"status": "running",
"environment": { "type": "jupyter", "label": "Jupyter Lab", "port": 8888 },
"resources": { "cpu": "4", "memory": "16GB", "disk": "20GB" },
"organizationId": "org_xyz",
"owner": "user_456",
"url": "/api/workspace-proxy/ws_ghi789/lab",
"createdAt": "2025-01-16T11:00:00Z",
"updatedAt": "2025-02-01T08:00:00Z"
}
],
"meta": {
"total": 8,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
POST /api/v1/workspaces
Create a new workspace.
Scope: workspaces:write
Request Body
The request body has full parity with the Create Workspace UI: choose the IDE, size it (with custom resources or a saved environment), set a persistent volume size, attach services, environment variables, coding assistants, and optionally a distributed compute cluster.
{
"name": "Training Environment",
"description": "GPU-enabled workspace for model training",
"environmentType": "jupyter",
"projectId": "proj_abc123",
"customResources": {
"cpu": "4",
"memory": "16GB",
"disk": "20GB",
"gpu": "1",
"gpuType": "nvidia-a10g"
},
"workspaceVolumeSize": "50GB",
"useSpot": false,
"environmentVariables": { "EPOCHS": "10" },
"dataSources": [],
"addons": [],
"aiGateways": [],
"workflows": [],
"codingAssistants": ["claude-code"],
"cluster": {
"engine": "ray",
"coordinator": { "cpu": "2", "memory": "4GB" },
"worker": { "cpu": "2", "memory": "4GB" },
"workers": 2,
"engineOptions": { "metricsDashboards": true }
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Workspace name |
description | string | Yes | Workspace description |
environmentType | string | Yes | The IDE the workspace opens: jupyter, vscode, rstudio, or custom. Independent of the hardware sizing. |
projectId | string | No | Project the workspace belongs to. The project's volume is mounted at /volumes/local/<name>. If omitted, a project is created automatically. |
sharedVolumeIds | string[] | No | The shared volumes to mount at /volumes/shared/<name> (code/ read-only). None unless listed. A volume you may not mount is refused with 400, by name, and one whose share is later taken away is dropped from the list. |
environmentId | string | No | Id of a saved environment (from list_environments) supplying the container image and the size. Use this for a specific software stack (for example an RStudio image with your R packages). Set either environmentId or customResources. |
environmentVersion | number | No | Pin a specific version of the selected environment. Omit for the latest. |
customResources | object | When no environmentId | The size when not using environmentId: { cpu, memory, disk, gpu?, gpu_type? }. The workspace runs at exactly this size; no size is assumed, and a create with neither customResources nor environmentId is refused. At least 0.5 CPU and 1 GB of memory (the platform services' share plus room for the IDE); a smaller workspace is refused at Start with the size that would do. disk is the container's ephemeral scratch disk (not the persistent volume). A size with gpu is checked against the GPU machine sizes allowed on the Compute page and refused with a message naming the machine it needs when none is allowed (an A100 or H100 needs the largest sizes). |
workspaceVolumeSize | string | No | Size of the persistent /workspace scratch volume (virtual environments, caches, scratch files), which survives stop/start and is deleted with the workspace; until you sync, the working copy of the code and the data files you changed are kept on it too (for example "50GB"). Code and data you keep belong in the project's volume. Distinct from customResources.disk. Default "20GB". |
useSpot | boolean | No | Run the workspace on spot capacity (cheaper; it can be reclaimed, and the workspace restarts on a new node with its volumes kept). Default false. |
spotFallback | boolean | No | With useSpot, fall back to on-demand capacity when no spot is available (default true); false waits for spot. |
customPort | number | No | Custom IDE only: the port the environment image serves its IDE on. Default 8888. |
proxyHeaders | array | No | Custom IDE only: extra request headers forwarded to your IDE, as { name, value }. Values may use ${BASE_PATH} / ${ORIGIN} / ${PATH} / ${REQUEST_URI} placeholders. |
environmentVariables | object | No | Key/value environment variables injected into the workspace container. |
dataSources | array | No | Ids of data sources to wire into the workspace (injected via STRONGLY_DATA_SOURCES). |
addons | array | No | Ids of add-ons to attach (injected via STRONGLY_SERVICES). |
aiGateways | array | No | Ids of AI models / gateways to make available (injected via STRONGLY_SERVICES). |
workflows | array | No | Ids of workflows to make available (injected via STRONGLY_SERVICES). |
codingAssistants | array | No | The coding-assistant CLI to install at startup: at most one of claude-code, codex, opencode (more than one is refused with 400). Available for Jupyter and VS Code only. |
skillIds | array | No | Ids of your Skill Library skills to install at each start, with the Strongly skill, for every coding assistant (chosen or not). Jupyter Lab and VS Code only; a skill you cannot read is refused with 400. |
codeSessionEnabled | boolean | No | Let an agent drive a terminal in the workspace (a code session). |
cluster | object | No | Attach a distributed compute cluster. engine is one of ray, dask, spark; plus coordinator: { cpu, memory }, worker: { cpu, memory, gpu?, gpuType? }, workers, optional autoscale: { enabled, maxWorkers }, and engineOptions: { metricsDashboards }. The cluster is provisioned on deploy, removed on stop, recreated on start, and deleted with the workspace. See Compute Clusters. |
Response 201 Created
The workspace is created stopped; start it to run it.
{
"data": {
"success": true,
"workspaceId": "ws_ghi789",
"buildInfo": {
"buildId": "b_123",
"workspaceId": "ws_ghi789",
"workspaceName": "Training Environment",
"status": "pending",
"progress": 0,
"logs": ["Starting workspace creation...", "Resources (Custom): CPU 4, Memory 16GB, Disk 20GB"],
"createdAt": "2025-01-16T11:00:00Z"
}
},
"meta": {
"requestId": "req_abc123"
}
}
Error 400 Bad Request when neither environmentId nor customResources is set, or the size is too small:
{
"type": "urn:strongly:problem:validation-error",
"title": "Validation failed",
"status": 400,
"detail": "Set the workspace CPU, memory and disk (or choose an environment)",
"code": "validation-error",
"requestId": "req_abc123"
}
GET /api/v1/workspaces/:id
Get a single workspace by ID.
Scope: workspaces:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
Returns the full Workspace object.
PATCH /api/v1/workspaces/:id
Rename a workspace, change its description, change the services it selects or its coding assistants and their skills (applied at its next start or restart), or set its environment variables. Send only the fields you are changing; any other field is refused with 400. The size, image, IDE and environment are fixed when the workspace is created; to change them, create a new workspace.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Request Body
{
"name": "Updated Training Environment",
"description": "Updated description",
"environmentVariables": { "EPOCHS": "10" }
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Workspace name |
description | string | No | Workspace description |
addons, dataSources, aiGateways, mlModels, workflows, featureStores, agents | array | No | The services the workspace selects, each list replacing the current one (its STRONGLY_SERVICES is built from them) |
codingAssistants | array | No | The coding-assistant CLI installed at each start, at most one of claude-code, codex, opencode ([] for none), replacing the current one. More than one is refused with 400. Jupyter and VS Code only. |
skillIds | array | No | Skill Library skills installed at each start for every coding assistant (chosen or not), replacing the current list. Jupyter Lab and VS Code only; a skill the workspace's owner cannot read is refused with 400. A selected skill that was deleted or is no longer shared stops the start, so remove it here. |
sharedVolumeIds | string[] | No | The shared volumes it mounts at /volumes/shared/<name> from its next start or restart, replacing the current list ([] for none). The project's volume always mounts. A volume the owner may not mount is refused with 400, by name. |
environmentVariables | object | No | Environment variables (string values), replacing the current set. They are applied when the workspace is deployed: its first start, or a start from error. |
Response 200 OK
{
"data": {
"success": true
},
"meta": {
"requestId": "req_abc123"
}
}
Errors
| Status | Code | Description |
|---|---|---|
400 | validation-error | The body has a field other than the three above (for example cpu or image), or an environment variable value is not a string |
409 | invalid-state | environmentVariables on a workspace that is already deployed |
DELETE /api/v1/workspaces/:id
Delete a workspace, running or stopped: a running one is stopped first. Its /workspace volume and anything not synced to its volumes are deleted with it. This action is irreversible.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 204 No Content
POST /api/v1/workspaces/:id/start
Start a stopped workspace. The call returns as soon as the start is accepted, with the workspace in starting; it does not wait for the IDE to come up. Poll GET /api/v1/workspaces/:id until status is running (the workspace url is then set) or error. The first start of a new workspace deploys it. Starting a workspace that is already running or starting changes nothing and returns 200.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"success": true,
"message": "Workspace is starting"
},
"meta": {
"requestId": "req_abc123"
}
}
POST /api/v1/workspaces/:id/stop
Stop a running workspace. Releases compute resources while preserving workspace state.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"success": true,
"message": "Workspace stopped successfully. Data in volumes has been preserved."
},
"meta": {
"requestId": "req_abc123"
}
}
POST /api/v1/workspaces/:id/restart
Restart a running workspace. Equivalent to stop followed by start.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"success": true
},
"meta": {
"requestId": "req_abc123"
}
}
GET /api/v1/workspaces/:id/status
Read the workspace's current status now. The platform records what it finds, so the workspace list and details page show the same status.
Scope: workspaces:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"status": "running",
"url": "/api/workspace-proxy/45TwJjGz6v9FWnAt3",
"error": null
},
"meta": {
"requestId": "req_abc123"
}
}
| Field | Description |
|---|---|
status | deploying, starting, running, stopping, stopped or error. |
url | Where the workspace IDE opens once it is running. |
error | Why the workspace is in error, for example a container that keeps crashing, an image that cannot be pulled, or a setup step that failed; null otherwise. |
A workspace is in error only when it has actually failed. A slow start stays starting for as long as it takes; there is no time limit that turns it into an error.
Errors
| Status | Code | When |
|---|---|---|
| 404 | not-found | No workspace with this ID that you can see. |
| 502 | backend-unavailable | The status could not be read right now. The workspace's status is left unchanged; try again. |
GET /api/v1/workspaces/:id/metrics
Measure a running workspace now. Each call takes a fresh measurement (about 6 seconds, including a 5 second network sample). Nothing is cached, and a value that cannot be measured is reported as an error rather than a zero.
Scope: workspaces:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"cpu": { "usageMillicores": 250.4, "limitMillicores": 1000, "percent": 25.0 },
"memory": { "usageBytes": 1073741824, "limitBytes": 2147483648, "percent": 50.0 },
"disk": { "usedBytes": 2092804096, "capacityBytes": 10464022528, "percent": 20.0, "path": "/workspace" },
"network": { "receiveBytesPerSecond": 5125.4, "transmitBytesPerSecond": 1500.2, "totalBytesPerSecond": 6625.6, "windowSeconds": 5.02 },
"gpu": null,
"responseTime": { "avgMs": 3.1, "minMs": 2.4, "maxMs": 4.9, "samples": 5, "urlPath": "/" },
"containers": [
{ "name": "workspace", "role": "workspace", "cpuMillicores": 250.4, "cpuLimitMillicores": 1000, "cpuPercent": 25.0,
"memoryBytes": 1073741824, "memoryLimitBytes": 2147483648, "memoryPercent": 50.0 },
{ "name": "vol-data", "role": "platform", "cpuMillicores": 1.2, "cpuLimitMillicores": 100, "cpuPercent": 1.2,
"memoryBytes": 20971520, "memoryLimitBytes": 268435456, "memoryPercent": 7.8 }
],
"platformContainersTotal": { "cpuMillicores": 1.2, "memoryBytes": 20971520, "count": 1 },
"measuredAt": "2026-09-23T04:03:40.331446+00:00"
},
"meta": {
"requestId": "req_abc123"
}
}
| Field | Description |
|---|---|
cpu, memory | Usage of your workspace container against the size you set. |
disk | Space used on the workspace volume mounted at /workspace. |
network | Bytes per second received and sent over the sampling window, so even light traffic shows a reading. |
gpu | { "percent": ... } GPU utilization for workspaces with GPUs; null otherwise. |
responseTime | Round trip time of the workspace IDE answering an HTTP request. |
containers | Every container in the workspace, with its usage and size. role is workspace for yours and platform for the containers the platform runs beside it, such as one per mounted data volume. |
platformContainersTotal | Combined usage of the platform containers. |
Errors
| Status | Code | When |
|---|---|---|
409 | invalid-state | The workspace is not running. |
500 | internal-error | A measurement could not be taken; the message gives the reason. |
GET /api/v1/workspaces/:id/logs
Retrieve logs for a workspace. Supports different log types for build, deploy, and runtime phases.
Scope: workspaces:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | No | Which log: build, deploy, or pod (the running IDE's own log, the Runtime log on the Logs tab; the default) |
Response 200 OK
{
"data": [
{
"timestamp": "2025-02-01T08:01:30Z",
"level": "info",
"message": "Starting Jupyter server..."
},
{
"timestamp": "2025-02-01T08:01:32Z",
"level": "info",
"message": "Jupyter server is running at https://0.0.0.0:8888"
}
],
"meta": {
"requestId": "req_abc123"
}
}
POST /api/v1/workspaces/:id/sync
Sync the running workspace to its durable volumes. For the project's volume, this commits and pushes the code half to its configured branch; for every mounted volume you may write, it records the data files you changed, each as a new version of that file. A shared volume's code is read-only, so its code result is { "skipped": "read-only" }. Unsynced work survives stop, start and restart; only deleting the workspace loses it. Until you sync, it is not on the volume, not visible to others, not versioned, and not what an app deploy builds from. Sync before you delete the workspace, hand work off, or deploy from the volume.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Response 200 OK
{
"data": {
"workspaceId": "ws_ghi789",
"synced": [
{
"volumeId": "vol_def001",
"name": "training-data",
"result": {
"volumeId": "vol_def001",
"base": "/volumes/local/training-data",
"code": { "committed": true, "pushed": true, "conflict": false, "rebased": false },
"data": { "committed": true, "changed": 1, "files": { "forecasts/weekly.csv": { "path": "forecasts/weekly.csv", "version": 3 } }, "refreshed": true }
}
},
{
"volumeId": "vol_sh002",
"name": "datasets",
"result": {
"volumeId": "vol_sh002",
"base": "/volumes/shared/datasets",
"code": { "skipped": "read-only" },
"data": { "committed": false, "reason": "no data changes", "refreshed": true }
}
}
]
},
"meta": {
"requestId": "req_abc123"
}
}
data.changed counts the files saved as new versions. refreshed means the workspace's data/ now shows every file's latest version; when that could not be done, refreshError says why (restart the workspace to see them).
With no volumes mounted, data is { "synced": [], "message": "This workspace mounts no code/data volumes" }.
When the code changed on the volume since the workspace's last sync, on the same lines, the volume's code result is { "conflict": true, "pushed": false, "files": ["app.py"] }: nothing is overwritten, and the merge waits for you to decide each file (below). Sync again once no files are left: that finishes the merge and pushes it.
GET /api/v1/workspaces/:id/sync/conflicts
The files still to decide after a Sync conflict, per mounted volume. A file you merged by hand in the IDE counts as decided once its conflict markers are gone.
Scope: workspaces:read
Response
{
"data": {
"workspaceId": "ws_abc123",
"volumes": [
{ "volumeId": "vol_def001", "name": "training-data", "merging": true, "files": ["app.py"] }
]
}
}
POST /api/v1/workspaces/:id/sync/resolve
Decide one conflicted file: keep your version or take the volume's.
Scope: workspaces:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
volumeId | string | Yes | The mounted volume the file is in |
path | string | Yes | The file, relative to the volume's code/ folder |
take | string | Yes | mine (this workspace's version) or theirs (the volume's) |
Returns the volume's remaining files, as GET .../sync/conflicts does.
POST /api/v1/workspaces/:id/sync/abort
Cancel a volume's waiting merge: the workspace's code goes back to how it was before the Sync (your own changes kept, not yet synced).
Scope: workspaces:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
volumeId | string | Yes | The mounted volume |
POST /api/v1/workspaces/:id/execute
Run a shell command inside the running workspace container and return its output. The workspace must be running.
Scope: workspaces:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workspace ID |
Request Body
{
"command": "python train.py --epochs 10",
"timeoutSec": 120,
"cwd": "/volumes/local/my-project/code"
}
| Field | Type | Required | Description |
|---|---|---|---|
command | string | Yes | The shell command to run |
timeoutSec | number | No | Kill the command after this many seconds |
cwd | string | No | Working directory to run the command in |
Response 200 OK
{
"data": {
"stdout": "Epoch 10 complete\n",
"stderr": "",
"exitCode": 0,
"timedOut": false
},
"meta": {
"requestId": "req_abc123"
}
}
On failure, stderr carries the error output and exitCode is non-zero. timedOut is true if the command exceeded timeoutSec.
GET /api/v1/workspaces/minimum-size
The smallest workspace that runs: cpu and memoryGb (the platform's services beside the IDE, in platform, plus room for it), the smallest compute-cluster coordinator per engine (clusterCoordinator), and the smallest job run (job). A smaller size is refused at create and at start.
Scope: workspaces:read
GET /api/v1/workspaces/planned-volumes
The volumes a new workspace in a project would mount: { volumes: [{ volumeId, name, scope }] }, the project's volume (scope local) and the shared volumes listed in the sharedVolumeIds query parameter, comma-separated (scope shared; none if omitted). A listed volume you may not mount is refused with 400, by name.
Scope: workspaces:read
| Query parameter | Type | Description |
|---|---|---|
projectId | string | The project the workspace would be in |
GET /api/v1/workspaces/:id/deletion-impact
Before deleting: whether it can be deleted and, if not, what uses it, as the delete dialog says.
Response 200 OK
{
"data": {
"canDelete": false,
"reason": "in-use",
"message": "... is in use by ... Stop or delete those first.",
"dependents": [{ "kind": "app", "items": [{ "id": "app-abc", "name": "forecast-dashboard" }] }]
}
}
When it can be deleted, canDelete is true and dependents is empty.
Scope: workspaces:read
GET /api/v1/workspaces/:id/volumes
The volumes the workspace mounts, as its Volumes tab lists them, each with its mount path, code source, owner and access. Paged.
Scope: workspaces:read
| Query parameter | Type | Description |
|---|---|---|
group | string | project (default): its project's volume. shared: the shared volumes chosen for it |
q | string | Name contains |
sort | string | name (default), createdAt or updatedAt |
sortDir | string | asc (default) or desc |
page | number | Page, from 1 |
pageSize | number | 1 to 100 (default 10) |
POST /api/v1/workspaces/:id/ports
Save a labeled quick link to a port (the Ports tab). Any port from 1024 to 65535 opens at <workspace URL>/port/<port>/ with or without a saved link; the link is a bookmark. Only the workspace's owner can open its ports.
Scope: workspaces:write
| Field | Type | Required | Description |
|---|---|---|---|
port | number | Yes | Port the app listens on (1024 to 65535) |
label | string | No | Label for the link |
Response 201 Created: the link, { port, label, url }.
DELETE /api/v1/workspaces/:workspaceId/ports/:id
Remove a saved port link. The app on that port keeps running and still opens by its port URL.
Scope: workspaces:write