Workspaces
Shardflux workspaces: keys, organizations and projects, API keys and tool tokens, persistent and session lifetimes, sharing and attribution.
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 open a key, a name you choose such as
customer-42/main or agent-7/task. The first open of a key creates the workspace; every later open returns the same
one.
const workspace = await cloud.workspaces.open({ key: 'customer-42/main', template: 'python-node-browser' });ws = sf.open(key="customer-42/main", template="python-node-browser")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... | 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 | Refuses with 409 conflict (details.reason: workspace_deleted). Keys are never reused. |
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. 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. A new organization starts on the Free plan. |
| 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 | Never reused | 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:
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
}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 conversation 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 |
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 issuedshard ws sessions acme/api lists the same from the command line.
Workspace size
A workspace gets a fixed CPU, memory and disk allocation when it starts:
- 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 and disk are the ceiling. Memory is your
memory_mibcap when you give one, otherwise the template's default (2,048 MiB forpython-node-browser). - Memory stays the same while the workspace runs. Changed caps apply at the next start or resume.
const big = await cloud.workspaces.open({
key: 'acme/build', template: 'python-node-browser',
caps: { cpu_millis: 4000, memory_mib: 8192, disk_gib: 20 }, // 1000 cpu_millis = 1 vCPU
});
console.log(big.ceilings, big.grants);disk_gib is the space for the workspace's own changes; the template's files do not count against it. Plan maximums
are in Pricing and limits.
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.
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
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);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.