Workspaces

Shardflux workspaces: keys, projects, API keys, lifetimes, sharing, full memory and CPU from boot with memory billed as used, and resize.

A workspace is a computer named by a key

A workspace is an isolated Linux microVM with its own kernel, disk and memory, started from a template. You never create one by id: you use a key, a name you choose such as customer-42/main or agent-7/task. The first use of a key creates the workspace; every later use returns the same one.

TypeScript
import { workspace } from '@shardflux/sdk';

const ws = workspace('customer-42/main', { template: 'default' });   // (0.15.0+) no request yet
await ws.exec('pip install -r requirements.txt && pytest -q');       // the first call creates the workspace
Python
from shardflux import workspace

ws = workspace("customer-42/main", template="default")  # (0.11.0+) no request yet
ws.exec("pip install -r requirements.txt && pytest -q")  # the first call creates the workspace
Shell
shard exec customer-42/main --template default -- pytest -q   # (0.10.0+)

The key is usually something your application already has: the customer, the project, the chat thread. An agent harness passes it through and never creates, resumes or tracks workspaces itself: ws.created says whether a call created the workspace, and ws.open() returns the full workspace for lifecycle calls. cloud.workspaces.open({ key, template }) (sf.open(key=..., template=...), shard ws open) opens a key explicitly and waits until it runs.

  • Calls made while the first call opens the key share that open.
  • create: false (create=False) never creates: it uses the key's workspace, and answers 404 not_found when the key has none.

Choose keys that name what the workspace belongs to: one per customer, per project, per agent task. A key is unique in your organization, 1 to 200 characters long, without control characters. Keys that start with sf: are reserved.

What opening a key does

The key... The first call, or open()
is new Creates the workspace from the template's latest published version and waits until it runs.
belongs to a running workspace Returns it at once.
belongs to a suspended workspace Resumes it and waits until it runs.
belongs to a workspace that is starting or resuming Joins that start. Concurrent opens of one key end up with one workspace and one operation.
belonged to a deleted persistent workspace While deletion is in progress, refuses with 409 conflict (details.reason: workspace_deleted). The key stays reserved until deletion finishes; it then opens a new workspace ID.

Opening never resets a workspace. For an existing key, template is ignored: a workspace keeps the template version it was created from for its whole life, and the template's immutable paths follow its newest published version. The SDKs wait until the workspace is running unless you pass wait: false (TypeScript) or wait=False (Python), or --no-wait to shard ws open.

Processful and file-first workspaces

A workspace's mode is fixed when its key is first opened.

Mode What it is
processful (the default) One VM that keeps its files, memory and running processes between tool calls. It is suspended when idle and can be resumed, forked and snapshotted. Everything else on these pages describes it.
file_first A versioned tree of the files under /home/user and no VM between commands. Each command runs as an execution in a fresh VM, and the files it changed become the next revision. Only files persist.

Open a file-first workspace with mode: 'file_first' (@shardflux/sdk 0.9.0+), mode="file_first" (shardflux 0.5.0+) or --mode file-first (CLI 0.5.0+). Reopening a key with the other mode is refused (409 conflict, details.reason: mode_mismatch). The workspace view reports mode, and a file-first workspace's tree_revision. See File-first workspaces for when to choose one.

Organizations and projects

Holds Notes
Organization Projects, members, billing and the plan Plan limits and usage allowances are counted for the whole organization. Your first organization can start the signup trial while trials last; additional organizations need a paid plan to run workspace compute.
Project Workspaces and project API keys An API key opens workspaces in its own project and sees that project's workspaces.

cloud.me() (TypeScript), sf.me() (Python) and shard whoami show which organization and project a key belongs to.

API keys and tool tokens

Two credentials are involved, and your code handles only the first one.

Project API key Workspace tool token
Looks like sfk_<key id>_<secret> A short-lived signed token
Created by You, in the console (API keys) or with shard setup or shard api-keys create (0.5.0+); shown once The SDK, CLI or MCP server, from the API key
Scope One project; its tool permissions One workspace, a set of tools and one agent session
Lifetime Until revoked, or until the expiry you chose (30 days, 90 days, 1 year or never) At most 15 minutes; refreshed automatically
Used for Opening, listing and managing workspaces (https://api.shardflux.dev/v1) Tool calls inside the workspace: exec, files, terminal and so on

Keep API keys on your servers. The SDKs obtain tool tokens, reuse them until shortly before they expire, and fetch new ones when a workspace moves or resumes. The MCP server never returns tool tokens to the model.

A key's tool permissions decide what code with it can do inside a workspace. A key needs at least one of them to open, suspend, resume, fork or delete workspaces.

Tool Console label Allows
exec Run commands Run commands and follow their output
files Read and write files Read, write, list and remove files
pty Terminal sessions Interactive terminals
process Background processes List and signal processes
git Git Clone, status and commit
browser Browser Screenshots and rendered page content from the workspace's headless browser

Revoking an API key stops the tool tokens it obtained within 30 seconds. Tool tokens are issued at most 3,600 times an hour per API key (429 rate_limited beyond that); the SDKs reuse tokens, so normal use stays far below this.

Persistent and session workspaces

Every workspace has a lifetime, fixed when it is created.

persistent (the default) session
Ends Only when you delete it On close(), or after it has been idle for its idle timeout (10 minutes unless the template sets one)
When it ends - It is deleted with its files
The key afterwards Opens a new, empty workspace with a new id once deletion finishes Opens a new, empty workspace with a new id
When idle Suspends automatically, keeping memory and processes Counts toward the idle timeout
Suspend Allowed Refused (409, details.reason: session_lifetime)
Keep its state Nothing to do Fork it; the fork is persistent unless you ask otherwise

Use sessions for throwaway jobs:

TypeScript
const ws = await cloud.workspaces.open({ key: `job-${jobId}`, template: 'python-node-browser', lifetime: 'session' });
try {
  await ws.cell().exec.run(['python3', 'job.py']);
} finally {
  await ws.close();   // ends a session; for a persistent workspace it only closes local streams
}
Python
with sf.open(key=f"job-{job_id}", template="python-node-browser", lifetime="session") as ws:
    ws.exec("python3 job.py")
# Leaving the block called ws.close().

Disconnecting, a closed WebSocket or an expired token never ends a session. Reopening a live key with a different lifetime is refused with 409 (details.reason: lifetime_mismatch).

Share a workspace between agents and sessions

Any code holding an API key of the project can open the same key and gets the same workspace: a planner, a coder and a reviewer agent, or today's session and tomorrow's. They see one filesystem and the same processes.

Each tool token is issued for an agent session: one per workspace, principal and agent label. The label is how you tell the agents' work apart.

Client Default label Set it with
TypeScript SDK default open({ agentLabel }), workspace.cell({ agentLabel }), workspaceTools(workspace, { agentLabel })
Python SDK default sf.open(..., agent_label=...), ws.cell(agent_label=...), workspace_tools(ws, agent_label=...) (0.4.0+)
CLI cli --agent-label, or SHARDFLUX_AGENT_LABEL
MCP server mcp SHARDFLUX_AGENT_LABEL
TypeScript
const coder = await cloud.workspaces.open({ key: 'acme/api', template: 'python-node-browser', agentLabel: 'coder' });
const reviewer = await cloud.workspaces.open({ key: 'acme/api', template: 'python-node-browser', agentLabel: 'reviewer' });

const sessions = await cloud.workspaces.agentSessions(coder.id);   // label, principal and tokens issued

shard ws sessions acme/api lists the same from the command line.

Workspace size

A workspace sees its full CPU and memory from the moment it starts, and pays for the memory it actually holds:

  • The ceiling for each resource is the smallest of the template's limit, your caps and your plan's per-workspace maximum. Caps above the plan are clamped, not refused.
  • CPU: the workspace boots with the vCPUs of its plan's or template's maximum, and cpu_millis (the ceiling) is the CPU time it may use across them. Raising or lowering it, up to that maximum, applies live.
  • Disk grows on its own as it fills, up to the plan's and the template's maximum (with overage on, up to 2,048 GiB). Retained state counts the bytes the workspace keeps, not the disk's size.
  • Memory is elastic: the workspace sees the whole ceiling from boot, up to 8 GiB with python-node-browser on a paid plan, and holds what its processes use, 1 GiB while it is idle. You pay for the memory it holds. memory_mib sets a lower maximum and memory_mib_held the floor it keeps while idle.
  • Resize any workspace at any time, running or suspended. Caps given when you open an existing workspace apply the next time it boots.
TypeScript
const big = await cloud.workspaces.open({
  key: 'acme/build', template: 'python-node-browser',
  caps: { cpu_millis: 4000, disk_gib: 20 },   // 1000 cpu_millis = 1 vCPU of CPU time; memory up to the plan's maximum
});
console.log(big.memory);   // { allocation_mode: 'elastic', promised_mib: 8192, held_mib: 1024, plugged_mib: 0 }

disk_gib is the space for the workspace's own changes; the template's files do not count against it. A new workspace starts with its template's default, 20 GiB on the platform templates. An explicit caps.disk_gib at open sets the size it starts at, within the plan and template maximums. As the workspace fills it, the disk grows online on its own, half again at a time (at least 10 GiB), up to the smaller of the plan's and the template's maximum, with no restart and nothing for your code to do. Reopening without a disk cap keeps its disk. Plan maximums are in Pricing and limits.

Elastic memory

memory_mib is the most memory a workspace may use, not what it holds. The workspace sees all of it from the moment it boots, and its host backs memory as the workspace touches it. It holds what its processes use, at least a floor, memory_mib_held (default 1,024 MiB, at least 512). Memory its processes free goes back to the host as they free it, and file cache the workspace no longer reads is given back in the background. You pay for the memory it holds (RAM GiB-hours), so a workspace has room for a large build without paying for that room all day.

  • Every command starts at once. Package installs, test runners, builds, type checkers, linters, browsers and containers size themselves for the full maximum, because the workspace has it from boot.
  • Workspaces started earlier grow before heavy commands. Until its next cold start, a workspace started before full-memory boots adds memory as it grows: before a heavy command it grows to 4 GiB (or to memory_mib when that is lower), and that exec session carries memory_grow: outcome (delivered; partial when it got less than it asked for), from_mib, want_mib, got_mib and deliver_ms. resource_hint: "heavy" grows it before any command, "light" starts the command at once: resourceHint in TypeScript (0.14.0+), resource_hint in Python (0.10.0+) and the MCP server's exec tool (0.8.0+), --resource-hint in the CLI (0.9.0+). Every workspace accepts the hint.
  • The workspace view has memory: allocation_mode, promised_mib (the maximum), held_mib (the floor) and plugged_mib (what it holds above the floor, refreshed about every minute).
  • Caps. memory_mib sets the maximum and memory_mib_held the floor. Given caps replace the stored ones; a workspace opened again keeps its floor unless you give one, and a fork without caps keeps its source's. A fork applies its memory caps as it starts: a command started before they take effect waits for them, and its exec session reports the change in memory_grow. A floor within 512 MiB of the maximum is lowered so the workspace has room to grow. allocation_mode is deprecated: it is accepted and ignored.

Resize a workspace

Change the memory, CPU or disk of any workspace, running or suspended, without a restart:

TypeScript
const r = await workspace.resize({ memoryMib: 6144 });
console.log(r.memory);   // a 4 GiB maximum raised: { applies_at: 'now', previous_mib: 4096, applied_mib: 6144, held_mib: 1024, ... }
Python
r = ws.resize(memory_mib=6144)
print(r.memory)
Shell
shard ws resize acme/build --memory 6144

The new caps are stored, so every later start uses them, and the answer says for each resource when the change applies:

applies_at The change
now Applies live: memory grows or shrinks with every process and its memory kept, CPU changes up to the vCPUs the workspace booted with (its plan's or template's maximum), and the disk grows online.
resume The workspace is suspended: it resumes at the new size, with its memory and processes, before its first call.
next_start Applies the next time the workspace boots; reason says why, for example more vCPUs than it started with. Nothing restarts now.

Each resource also carries at_maximum: true when its target is the most the workspace can have, the smaller of the plan's and the template's maximum. A workspace already uses its full CPU and memory from boot and its disk grows on its own as it fills, so resize to lift caps you set lower, not to speed up a build. With equal previous and target values, nothing changed.

A lower maximum applies live too: the workspace gives back the memory its processes are not using; when they use more, it keeps it and the answer says limit_reason: "shrink_stalled". Resize with workspace.resize() (TypeScript 0.14.0+), ws.resize() (Python 0.10.0+), shard ws resize (CLI 0.9.0+), the MCP tool workspace_resize (0.8.0+) or PATCH /v1/workspaces/{id}/caps.

Resize only the capacity you need

Use atLeast: true in TypeScript (0.16.0+) or at_least=True in Python (0.12.0+) to grow each named resource independently. A smaller or equal disk stays at its current size, so the same request can grow memory. The response keeps each resource's requested, previous, target and applied values; equal previous and target values mean it stayed unchanged.

TypeScript
const result = await workspace.resize({ memoryMib: 6144, diskGib: 2, atLeast: true, wait: false });
console.log(result.operationId, workspace.caps);

wait: false returns the server's held answer, up to 20 seconds, with the operation and stored caps. Per-resource results include applies_at once the resize has decided them. You can follow a pending operation with cloud.workspaces.waitForOperation(result.operationId). The default waits for the resize to finish.

workspace.caps (TypeScript) and ws.caps (Python) expose the stored caps used at every later start. A null numeric cap means the workspace uses its template and plan bounds: without memory_mib, its maximum is the smaller of the plan's and the template's maximum.

An existing workspace whose resize reports reason: legacy_layout can enable live memory resizing with await ws.upgrade({ at: 'now', wait: true }) (TypeScript 0.16.0+, Python 0.12.0+: ws.upgrade(at="now", wait=True)). The cold start preserves files and installed package files and ends memory and processes. A following resize can apply live. See upgrade while keeping your files.

Give a workspace credentials

Store a credential once as a secret and bind it to workspaces by name. Every command and terminal in the workspace gets it as an environment variable, and no API ever returns the value.

TypeScript
const project = (await cloud.me()).api_key!.project_id;
await cloud.secrets.create(project, { name: 'OPENAI_API_KEY', value: process.env.OPENAI_API_KEY! });
const ws = await cloud.workspaces.open({ key: 'customer-42/main', template: 'python-node-browser', secrets: ['OPENAI_API_KEY'] });

On an existing key, secrets replaces the binding; leaving it out keeps it. Forks keep the binding. Details are in the TypeScript and Python references.

Find and list workspaces

TypeScript
const byKey = await cloud.workspaces.findByKey('customer-42/main');   // null when no workspace has the key
for await (const ws of cloud.workspaces.listAll({ keyPrefix: 'customer-42/' })) console.log(ws.key, ws.state);
Python
ws = sf.workspaces.get_by_key("customer-42/main")  # None when no workspace has the key
for w in sf.workspaces.list_all(key_prefix="customer-42/"):
    print(w.key, w.state)

Lists show persistent workspaces by default. Pass lifetime (persistent, session or any) to see sessions. Lookups by key search every lifetime and prefer the live workspace over ended ones.

Labels and key reuse

Workspace labels are searchable string metadata: at most 50 pairs, keys 1–64 characters (A-Z, a-z, digits, ., _, /, -, starting with a letter or digit), values up to 256 characters. Open accepts labels; supplying them replaces the map. List accepts a JSON labels filter with exact matches for every supplied pair. PUT /v1/workspaces/{id}/labels replaces the map; {} clears it. Labels are not a secret store. SDK 0.12.0 and Python 0.8.0 expose these directly.

Deletion reserves a persistent key until teardown finishes (observed_state: deleted), then the key may create a new workspace ID. Old IDs, revocations and history stay deleted. Reopening an existing live or failed workspace never resets its disk.

View this page as Markdown