File-first workspaces

File-first workspaces keep a versioned file tree and run each command in a fresh VM: executions, tree revisions, limits, and when to choose them.

A workspace that is only its files

A file-first workspace has no VM between commands. Its state is a versioned tree of the files under /home/user. File calls (read, write, list, search, patch, remove, move) work on that tree directly, without a VM. Each command runs as an execution: a fresh VM starts from the workspace's template with the tree's files in place, runs the command, and the files the command changed under /home/user become the next revision of the tree. Nothing else survives an execution: processes, memory and files outside /home/user are gone when it ends.

File-first workspaces are opt-in. The default, a processful workspace, keeps one VM with its processes and memory between calls and is unchanged. The mode is chosen when a key is first opened and never changes.

Client Version Open a file-first workspace
TypeScript SDK @shardflux/sdk (0.9.0+) cloud.workspaces.open({ key, template, mode: 'file_first' })
Python SDK shardflux (0.5.0+) sf.open(key=..., template=..., mode="file_first")
CLI @shardflux/cli (0.5.0+) shard ws open <key> --template <slug> --mode file-first
MCP server @shardflux/mcp (0.4.0+) workspace_open with mode: "file_first", or SHARDFLUX_WORKSPACE_MODE=file_first
HTTP API /v1 POST /v1/workspaces/open with "mode": "file_first"

When to choose file-first

Processful (default) File-first
Between tool calls A VM with its processes and memory; suspended when idle Only the files; no VM
A command An exec session: output streams while it runs, and it can keep running in the background An execution: the call returns when the command has ended, with its output
What persists Disk, memory and running processes Files under /home/user
File calls Served by the workspace's VM (a suspended workspace is woken for changes) Served from the tree; never wait for a VM
Tools Commands, files, terminals, processes, git and browser Commands (as executions) and files
Lifecycle Suspend, resume, fork, snapshot, reset, save as template Open and delete
Running workspaces at once Counts while it runs Never counts
Compute RAM GiB-hours for as long as it runs Only while an execution runs

Choose file-first when an agent mostly reads and edits files and runs commands that finish: tests, builds, linters, scripts, code generation. Nothing has to stay running between its tool calls, file calls never wait for a VM to start or wake, every change is a numbered revision, and an idle workspace uses no compute and no running slot.

Choose processful when anything must keep running or stay in memory between calls (a dev server, a REPL, a database, a terminal session), when you install software outside /home/user between commands (for example with apt), or when you need terminals, the process, git or browser tools, forks, snapshots or shared volumes.

Each execution starts a VM, so a short command takes longer end to end than on a running processful workspace. Git and a browser still work inside one command: git clone https://... /home/user/repo in an execution leaves the repository in the tree, and a script can drive the template's browser while it runs.

Open a file-first workspace

TypeScript
const ws = await cloud.workspaces.open({ key: 'customer-42/repo', template: 'python-node-browser', mode: 'file_first' });
ws.mode;          // 'file_first'
ws.treeRevision;  // 0: the empty tree

await ws.cell().files.write('/home/user/app/main.py', 'print("hi")\n', { createParents: true });   // revision 1
Python
ws = sf.open(key="customer-42/repo", template="python-node-browser", mode="file_first")
ws.mode, ws.tree_revision  # ("file_first", 0)

ws.files.write("/home/user/app/main.py", "print('hi')\n", create_parents=True)  # revision 1
Shell
shard ws open customer-42/repo --template python-node-browser --mode file-first
shard files write customer-42/repo /home/user/app/main.py --from main.py     # prints the new tree revision
  • The open answers at once with a tool token: no VM starts, so there is nothing to wait for.
  • A file-first workspace is always persistent. lifetime: 'session' is refused (422 validation_failed, details.reason: not_supported_for_mode).
  • Reopening the key without mode keeps its mode. Reopening it with the other mode is refused (409 conflict, details.reason: mode_mismatch).
  • The template version must support layered disks; otherwise the open is refused (409 conflict, details.reason: layout_unsupported).
  • caps size each execution's VM, as they size a processful workspace (see Workspace size). Caps passed to a later open apply from then on.

Run commands as executions

TypeScript
const r = await ws.executions.run(['bash', '-lc', 'cd app && python3 main.py > out.txt && cat out.txt'], { timeoutMs: 600_000 });
r.state;          // 'succeeded' (the command ran to its end), 'failed' or 'lost'
r.exitCode;       // 0
r.stdoutText;     // 'hi\n' (r.stdout holds the bytes)
r.changed;        // [{ path: '/home/user/app/out.txt', change: 'added', type: 'file' }]
r.treeRevision;   // 2
Python
run = ws.executions.run("cd app && python3 main.py > out.txt && cat out.txt", timeout=600)
run.ok, run.exit_code, run.text()  # (True, 0, "hi\n")
[(c.change, c.path) for c in run.changed]  # [("added", "/home/user/app/out.txt")]
run.tree_revision  # 2
Shell
shard ws exec customer-42/repo -v -- bash -lc "cd app && python3 main.py > out.txt && cat out.txt"

The call returns when the command has ended. The result has:

Field Meaning
state succeeded: the command ran to its end, whatever its exit code; check exit_code and timed_out. failed: the command could not be run or its result could not be published. lost: the execution's host stopped answering. A failed or lost execution changed nothing.
exit_code, term_signal, timed_out How the command ended (exit_code is -1 when a signal ended it).
stdout, stderr The output, each up to the output limit (1 MiB by default, up to 16 MiB); stdout_truncated and stderr_truncated say whether more was cut.
base_revision, tree_revision The revision the command ran on, and the revision after it: base_revision + 1 when it changed files, base_revision when it changed nothing, null unless it succeeded.
changed What the command added, modified or deleted under /home/user (path, change, type), up to 10,000 entries (changed_truncated).
error For failed and lost: code, message, retryable and details.reason, for example exec_failed_to_start or lease_expired. retryable: true means a new execution may succeed.
timings Milliseconds: queued, run and publish time, and the host's steps.
  • Output is not streamed; it comes with the result.
  • One execution runs at a time. While it runs, a second execution and every file change are refused with 409 workspace_busy (details.reason: execution_in_progress, details.execution_id). The SDKs and the CLI wait for the running one, within their transition timeout (120 seconds by default).
  • An execution cannot be canceled. Aborting the call, or Ctrl-C in the CLI, stops waiting; the command runs to its end and its result can be fetched later.
  • The command sees the template's files too. They are not part of the tree until a command changes one. A template file under /home/user that a command deletes is back at the next execution.
  • cwd must be under /home/user. env, stdin (up to 1 MiB), timeout and secret_refs work as for commands on a processful workspace.

Execution ids make retries safe

Every execution has an id: 8 to 128 characters of A-Z a-z 0-9 . _ : -, starting with a letter or a digit. The SDKs and the CLI generate ex-<uuid> unless you pass one (executionId, execution_id, --execution-id).

  • The same id with the same request returns the recorded result and never runs the command again (replayed; HTTP 200 instead of 201). The same id with a different request is refused (409 conflict, details.reason: execution_id_reused).
  • The SDKs and the CLI retry network failures and 503 service_unavailable with details.reason: no_execution_host (no host had room; Retry-After given) with the same id, so a retried call runs the command at most once. Pass your own id to make a call safe to repeat across processes too.
  • They never switch to a new id by themselves. After a failed or lost execution, run it again with a new id if you want to.
  • Read a result again with ws.executions.get(id) (TypeScript, { waitMs } to wait for a running one), ws.executions.get(id, wait=...) (Python), shard executions get <key> <id> [--wait 10m] or GET /v1/workspaces/{id}/executions/{execution_id} on the cell endpoint (202 while it runs). Results are kept for 7 days.

Tree revisions

Revision 0 is the empty tree. Every change that changes something publishes the next revision: a write, patch, remove, directory creation or move, and an execution that changed files.

  • Every files response carries the revision it was served from or produced, in X-Tree-Revision. The SDK handles track the newest one they have seen (ws.treeRevision, ws.tree_revision); shard ws get prints it.
  • A change can require a revision: ifTreeRevision (TypeScript), if_tree_revision (Python), --if-revision (CLI) or If-Match: <revision> (HTTP). If the tree has moved on, nothing changes and the call is refused with 409 conflict, details.reason: tree_revision_mismatch and details.current_tree_revision; the SDKs raise TreeRevisionMismatchError. Read what changed, then retry with the new revision.
  • Without a condition, a change that races another writer is applied to the newest revision.
  • A file's own revision (the SHA-256 of its content) and expected_revision on a patch work as on a processful workspace, for files of any size (see Search and edit files).
TypeScript
import { TreeRevisionMismatchError } from '@shardflux/sdk';

try {
  await ws.cell().files.write('/home/user/app/main.py', 'print("bye")\n', { ifTreeRevision: ws.treeRevision! });
} catch (err) {
  if (!(err instanceof TreeRevisionMismatchError)) throw err;
  console.log('the tree is at', err.currentTreeRevision);   // nothing changed
}

Paths. Only paths under /home/user exist. / and /home are directories that cannot change. A read elsewhere is 404 not_found and a change elsewhere 422 validation_failed, both with details.reason: outside_tree_root. File names must be valid UTF-8. A file's modified_at is the time of its revision, and uid and gid are not reported.

Size. Every change records the whole tree, so changes take longer as the tree grows: about 0.6 seconds per change at 100,000 files.

What a file-first workspace cannot do

These calls are refused with 409 conflict and details.reason: not_supported_for_mode (details.mode, details.operation). The SDKs refuse them before sending anything (NotSupportedForModeError, local: true), the CLI exits 1 with a hint, and the MCP server returns the error with a hint.

Refused Instead
Suspend and resume Nothing to do: no VM runs between executions.
Snapshot, fork, reset, save as template Use a processful workspace.
Exec sessions (exec.start, output, attach, signal, cancel), terminals, processes Run each command as an execution.
The git and browser tools Run git or a browser script inside an execution.
Workspace changes against the template, shared volumes, idle policies, keepalives -

A file-first workspace is never suspended, so nothing wakes: wake() returns false and the wake hint answers resident.

On a processful workspace, reading an execution is refused with 409 conflict and execution_id in an exec request with 422 validation_failed, both with details.reason: not_supported_for_mode. The cell ignores If-Match there, so the SDKs refuse ifTreeRevision / if_tree_revision on a processful workspace before sending anything.

Billing and limits

  • A file-first workspace never counts toward your plan's running workspaces at once, idle or not.
  • Each execution is metered like a workspace that runs for as long as the execution does: RAM GiB-hours at the execution VM's memory, and the CPU time the command used. Between executions nothing is metered for compute.
  • Retained state counts the tree's current revision.
  • An open is admitted like a start: the plan's allowances and billing state apply (402 allowance_exhausted, 402 entitlement_required).

The limits are in Pricing and limits.

Agent tools and the MCP server

  • TypeScript (0.9.0+): workspaceTools(ws) of a file-first workspace offers exec and the files tools only. Its exec runs an execution and adds execution_id, state, tree_revision and changed (up to 200 paths, changed_truncated) to the result. workspaceTools(ws, { mode }) builds the definitions without reading the workspace, and onExecution(id) is called with each execution id before it is sent.
  • MCP server (0.4.0+): exec runs an execution in the same way. A server pinned to a file-first workspace lists only the tools that mode has and tells the client when that list changes (notifications/tools/list_changed).
  • Python: workspace_tools() does not run executions yet; its exec tool refuses a file-first workspace with not_supported_for_mode. Use ws.executions.run().

See Errors for every refusal and the HTTP API for the requests.

View this page as Markdown