Freestyle Docs

Freestyle / Guides

Migrate from E2B to Freestyle

Map E2B workspaces to Freestyle VMs with commands, streaming PTY sessions, files, snapshots, previews, and scoped client access.

Move your E2B JavaScript SDK v2 integration to Freestyle with the mappings below. Use exec for commands and Git, PTY sessions for streaming or long-running work, and snapshots for reusable environments.

For notebook and desktop workloads, see Jupyter and GNOME.

Why Migrate

Freestyle provides full Linux VMs for workspaces that live for hours or weeks. A VM can keep running without a timeout heartbeat, pause with its memory intact, and resume the same processes later.

  • Reusable running environments. Snapshots capture memory and disk, including a dev server that is already listening.
  • A complete Linux machine. Ubuntu includes systemd, Docker, Node.js, Bun, Python, and Git. Nested virtualization, FUSE, eBPF, and WireGuard are available for workloads that need them.
  • Control over resources and networking. Resize a VM as it grows, isolate tenants with VPCs, and use TLS rules to hold provider credentials at the edge.

See base snapshots for image contents and sizes, and pricing and limits for plan capacity.

Map Your Current Setup

Map each call to the operation you need. Commands and Linux tools run inside the VM; files, sessions, snapshots, and networking have SDK APIs.

E2BFreestyle
Sandbox.create(template, opts)freestyle.vms.create({ snapshotId, firewall, ... })
Sandbox.connect(sandboxId)freestyle.vms.ref(vmId); commands and file writes wake a paused VM, file reads leave it paused
sandbox.sandboxIdvmId, or the slug you chose
metadata, Sandbox.list({ query })metadata, freestyle.vms.list({ metadata, state })
sandbox.getInfo(), isRunning()vm.data() and its state
timeoutMs, setTimeout()Choose a lifecycle policy; extend deletion with vm.update({ ttlSeconds })
lifecycle: { onTimeout: "pause" }maxRunSeconds for a run budget, or vm.pause() when your application lease expires
sandbox.pause()vm.pause()
sandbox.kill()vm.delete()
sandbox.fork(), createSnapshot()vm.snapshot(), then vms.create({ snapshotId })
commands.run(cmd, { cwd, envs, user, timeoutMs })vm.exec({ command, env, linuxUser, timeoutMs })
commands.run(cmd, { background, onStdout })vm.pty.open({ exec: cmd, onData, onExit })
commands.connect(pid), kill(pid)vm.pty.attach({ session }), vm.pty.close()
pty.create(), sendInput(), resize(), kill()vm.pty.open(), session.write(), resize(), pty.close()
files.read(), write(), list(), getInfo()vm.fs.readTextFile(), writeTextFile(), readDir(), stat()
files.read(path, { format: "bytes" }), write(path, bytes)vm.fs.readFile() (a Uint8Array, not a Buffer), writeFile()
files.write([...entries])Promise.all of writeFile calls
files.read(path, { format: "stream" })vm.fs.readFileStream()
files.rename()vm.exec("mv ...")
files.watchDir()inotifywait in a PTY session
uploadUrl(), downloadUrl()Your server authorizes and streams transfers through vm.fs
Git operationsvm.exec("git ..."); private Git credentials can stay at the edge
sandbox.getHost(port)A TLS rule from a style.dev name or your domain to the port
allowPublicTraffic: false, traffic tokenForward auth on the ingress rule
allowInternetAccess, allowOutFirewall rules; egress is denied until allowed
envs at createenv on each exec, or bake into the snapshot
envd access token for clientsIdentity tokens
Template().fromImage().runCmd()..., Template.build()Boot a base snapshot, set it up, vm.snapshot()
setStartCmd(cmd, readyCmd)Start a systemd unit, wait for it, snapshot while running
template:tagSnapshot slug
Secrets injected into requestsTLS transforms

Create, Find, And Reconnect

Install freestyle and set FREESTYLE_API_KEY in your backend. Create a workspace with a snapshot, an explicit firewall, and a slug you choose:

import { Freestyle } from "freestyle";

const freestyle = new Freestyle();
const { vm, vmId } = await freestyle.vms.create({
  snapshotId: "my-template", // omit for the Ubuntu base snapshot
  slug: `workspace-${workspaceId}`,
  metadata: { workspaceId, tenant: "acme" },
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
});

Reconnect with a handle by ID or slug. vms.ref() makes no network call:

const workspace = freestyle.vms.ref(`workspace-${workspaceId}`);
const result = await workspace.exec("uptime");
const info = await workspace.data();
const { vms } = await freestyle.vms.list({ metadata: `workspaceId:${workspaceId}` });

Commands and file writes wake a paused VM. File reads and directory listings leave it paused. data().state reports starting, running, pausing, paused, or stopped; a missing VM returns a FreestyleApiError with status 404.

Use vm.pause() to preserve the workspace and vm.start() to resume it. vm.delete() permanently removes the VM and the firewall and TLS rules that name it.

Replace The Timeout Heartbeat

Freestyle has no default running-time deadline. Keep a VM running until your application pauses or deletes it, or configure a lifecycle policy:

NeedFreestyle
Pause after network inactivityidleTimeoutSeconds
Pause after a fixed duration of one runmaxRunSeconds
Limit total running time across resumesmaxRunTotalSeconds
Delete at a deadlinettlSeconds, measured from createdAt and extendable with vm.update()
Delete after time spent paused or stoppedautoDeleteSeconds
Pause when an application lease expiresCall vm.pause() from your backend when the lease expires

The lifecycle guide shows how to set and extend a deadline. TTL deletion is asynchronous, so use it for lifetime cleanup rather than exact-time scheduling.

Idle time measures network activity. PTY output and CPU work alone do not reset it; leave idleTimeoutSeconds unset for a long-running job that must finish without user input. Some plans cap how long an unused VM is kept; vm.data().autoDeleteSeconds reports that policy.

Run Commands

commands.run() in the foreground becomes vm.exec(). The options rename: envs is env, user is linuxUser, and cwd is a cd inside the command string, which the guest runs through its shell. Two behaviors differ:

  • Exit status is a value, not an exception. E2B throws CommandExitError on a non-zero exit. Freestyle returns { stdout, stderr, statusCode } and leaves the check to you. statusCode is null when the timeout killed the command.
  • The timeout is capped at five minutes. timeoutMs accepts up to 300000. Use a PTY for longer commands or streaming output.
// commands.run("npm test", { cwd: "/project/workspace", envs: { CI: "1" }, user: "node", timeoutMs: 120_000 })
const result = await vm.exec({
  command: "cd /project/workspace && npm test",
  env: { CI: "1" },
  linuxUser: "node",
  timeoutMs: 120_000,
});

if (result.statusCode !== 0) {
  throw new Error(`tests failed: ${result.stderr}`);
}

exec starts in the user’s home directory and runs as the VM’s default user, the account holding uid 1000, unless linuxUser names another. On the Ubuntu base snapshots that is ubuntu, with passwordless sudo; E2B’s default was user. If your image relies on a node account, create it in the snapshot and name it on every call, or use vm.linuxUser("node") once and call exec and pty on that handle.

stdin supplies up to 1 MiB as a base64 string. For interactive input, use a PTY and session.write().

Stream And Detach Long Commands

Use vm.pty.open({ exec }) for installs, builds, agents, and other long-running commands. It streams output through onData, reports the exit code through onExit, and keeps running when the client disconnects. Name the session so another client can attach to the same process:

const build = await vm.pty.open({
  exec: "cd /project/workspace && npm run build",
  slug: "build-42",
  onData: (bytes) => process.stdout.write(bytes),
  onExit: (code) => console.log("build exited", code),
});

build.detach(); // disconnect; the build keeps running

const attached = await vm.pty.attach({
  session: "build-42",
  onData: (bytes) => process.stdout.write(bytes),
  onExit: (code) => console.log("build exited", code),
});

attached.signal("sigint"); // request cancellation
await vm.pty.close("build-42"); // kill and remove the session

PTY output combines stdout and stderr as terminal output. A named open() is get-or-create: an existing session is reused without rerunning exec. Use a new slug for a new job. PTY commands have no five-minute exec limit; the VM’s own lifecycle policy still applies.

For a supervised service that should restart after exiting, use systemd. See PTY sessions for input, signals, and reattachment.

Move Files

Use vm.fs for text, bytes, directory listings, and file metadata:

const text = await vm.fs.readTextFile("/project/workspace/package.json");
const bytes = await vm.fs.readFile("/project/workspace/logo.png"); // Uint8Array
await vm.fs.writeTextFile("/project/workspace/.env", "PORT=3000\n");
await vm.fs.writeFile("/project/workspace/data.bin", bytes, { mode: 0o644 });

const entries = await vm.fs.readDir("/project/workspace");
const info = await vm.fs.stat("/project/workspace/package.json");
await vm.fs.mkdir("/project/workspace/tmp");
await vm.fs.remove("/project/workspace/tmp");

Use Promise.all for batch writes and vm.exec("mv old new") for renames. For recursive copies, use freestyle vm scp.

For large transfers, upload a Blob with writeFile() and download with readFileStream(). The SDK streams and resumes chunked uploads; reads support offset and length. Files can be up to 16 GiB.

Authorize client uploads and downloads in your backend and stream them through vm.fs with the account API key. Identity tokens cover exec and PTY; they do not grant the root-level file API.

New files written through vm.fs are root-owned. Set ownership for your application user after uploading. See Files for streaming, permissions, and paused-VM reads.

Watch A Directory

Run inotifywait in a PTY session. Install inotify-tools in your snapshot, then stream the events from each workspace:

await vm.exec("sudo apt-get update && sudo apt-get install -y inotify-tools");
const watcher = await vm.pty.open({
  exec: "inotifywait -m -r -e create,delete,modify,moved_from,moved_to,attrib /project/workspace",
  slug: "watch-workspace",
  onData: (bytes) => process.stdout.write(bytes),
});

Reattach by session name after disconnecting; vm.pty.close("watch-workspace") stops the watcher. If you parse events, buffer terminal output across chunks until each line is complete. Output alone does not reset a VM idle timeout.

Terminals

Open a shell without exec, then relay its bytes to your terminal UI:

const session = await vm.pty.open({
  cols: 120,
  rows: 30,
  slug: "main",
  onData: (bytes) => socket.send(bytes),
});

session.write("ls -la\n");
session.resize({ cols: 200, rows: 60 });
session.signal("sigint");
session.detach(); // leave the shell running

Reattach with vm.pty.attach({ session: "main", onData }); close the process with vm.pty.close("main"). Select a user with vm.linuxUser("node").pty after creating that user in your snapshot.

Sessions survive disconnects and pause/resume. Start a paused VM before reattaching; the SDK requires an explicit attach() after a connection drops. For browsers, relay the authenticated WebSocket through your backend as in Run a Web Terminal.

Rebuild Templates As Snapshots

Translate your E2B template’s setup steps into a shell script and run it on an Ubuntu VM. Node.js, Bun, Python, Git, and Docker are already installed; check the base image inventory before adding packages.

The CLI runs the script, streams its output, and takes a snapshot only after it exits successfully:

freestyle snapshot create --base freestyle/ubuntu --script ./setup.sh --slug my-template

Create workspaces from that snapshot:

const { vm } = await freestyle.vms.create({
  snapshotId: "my-template",
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
});
Template stepSnapshot setup
runCmdA command in setup.sh, or vm.exec(); use a PTY for long-running steps
copyvm.fs.writeFile() or freestyle vm scp
setUser, setWorkdirCreate the Linux user, set ownership, then select linuxUser and cd in commands
setEnvsPass env to commands or set Environment= in a systemd unit
setStartCmd(cmd, readyCmd)Start a systemd unit and wait for readiness before capturing the snapshot
fromDockerfileBuild and run your Docker image inside the VM
Names and tagsGive the snapshot a slug; keep its immutable ID when you need a pinned version

A snapshot captures running processes as well as files, so a dev server started before capture is already running in each new workspace. For an SDK build, call vm.snapshot({ slug: "my-template" }) after setup completes. See snapshot builds and the Next.js guide for complete examples.

Publish Previews

sandbox.getHost(port) returns a hostname that exists for every port. On Freestyle you declare which ports are public: one TLS rule routes one hostname to one VM port. Any unused subdomain of style.dev is free and needs no DNS, or use a domain you have verified.

// const url = `https://${sandbox.getHost(3000)}`
const domain = `ws-${workspaceId}.style.dev`;

await freestyle.tls.rules.create({
  action: "allow",
  domain,
  source: { public: true },
  destination: { vmId, port: 3000 },
});

const url = `https://${domain}`;

The edge terminates HTTPS and forwards the connection to the guest port, so the dev server must listen on 0.0.0.0. Rules are deleted with the VM. A public preview request wakes a paused VM. With forward auth, the check runs first: denied requests and authorization failures leave the VM paused; allowed requests can wake it and resume compute billing.

Private previews. allowPublicTraffic: false plus the e2b-traffic-access-token header becomes forward auth. Freestyle calls your authorization endpoint for each request with the caller’s Authorization and Cookie headers and forwards only on a 2xx, so the check can use your existing session system. Reuse one authorizer across all your preview routes:

const auth = await freestyle.tls.forwardAuth.create({
  url: "https://api.acme.com/previews/authorize",
  headers: { authorization: `Bearer ${process.env.PREVIEW_AUTH_SECRET}` },
  timeoutMs: 1500,
  protectedCookies: ["__Host-acme-session"], // seen by your authorizer, never by the VM
});

await freestyle.tls.rules.create({
  action: "allow",
  domain,
  source: { public: true },
  destination: { vmId, port: 3000 },
  forwardAuth: { id: auth.id },
});

Your authorizer reads X-Freestyle-TLS-Rule-Id to learn which preview is being requested and returns 2xx to allow, 3xx to send a signed-out user to login, or 4xx to refuse. WebSocket upgrades pass through the same rule, so hot module reloading works behind it. A cookie-based sign-in handoff for many previews on one authorizer is in Protect A Sandbox With Forward Auth.

Hand Clients Scoped Access

E2B lets a backend hand its desktop or CLI clients the sandbox ID, the envd access token, and the traffic token so they can call the sandbox directly without the team API key. Freestyle’s equivalent is an identity: your server grants it one or more VMs, restricted to named Linux users, and mints a token the client uses as its own credential.

const { identity, identityId } = await freestyle.identities.create();
await identity.permissions.vm.grant({ vmId, allowedLinuxUsers: ["node"] });
const { id: tokenId, token } = await identity.tokens.create();

return { vmId, token }; // to the desktop app or CLI
import { Freestyle } from "freestyle";

const freestyle = new Freestyle({ identityAccessToken: token });
const workspace = freestyle.vms.ref(vmId).linuxUser("node");

await workspace.exec("git status");
const shell = await workspace.pty.open({ onData: render });

The grant carries allowedLinuxUsers, so a client holding it cannot run anything as root, and a call that names no user is refused. It also cannot touch VMs it was not granted or create account resources. Revoke a client with identity.tokens.revoke(tokenId) or identity.permissions.vm.revoke(vmId) without rotating anything else, and delete the identity when the user leaves. SSH accepts the same token, so ssh <vm>+node:<token>@beta-ssh.freestyle.sh gives a CLI a terminal with no code at all.

An identity token reaches only the exec and PTY routes. It cannot call vm.fs, which runs as root in the guest, so an E2B client that read and wrote files directly with its scoped session needs one of two changes. Route file traffic through your server, which holds the API key and can check the user’s permission on each request. Or move small transfers onto exec as the granted user, with stdin for uploads of up to 1 MiB and stdout for downloads, which keeps every byte under that user’s own permissions.

Control Egress

An E2B sandbox has Internet access unless allowInternetAccess: false, and allowOut and denyOut refine that. A Freestyle VM has none until a firewall rule allows it, and only allow rules exist.

E2BFreestyle
allowInternetAccess: true (default)firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] }
allowInternetAccess: falsefirewall: { rules: [] }
allowOut: ["1.1.1.1", "8.8.8.0/24"] with denyOut: allTrafficOne rule per range: destination: { cidr: "8.8.8.0/24" }
allowOut: ["api.github.com"]A TLS rule per exact domain, destination: { public: true }
allowOut: ["*.example.com"]Not available yet; only exact names steer
denyOut: [...] alongside broad allowNot available; start from nothing and allow what is needed
Secrets injected into requestsA transform on the TLS rule

A TLS egress rule does two things at once: it admits the VM to that domain with no broader Internet grant, and it can inject a header the guest never held. That is how you replace E2B secrets with a key that lives at the edge:

await freestyle.tls.rules.create({
  action: "allow",
  domain: "api.openai.com",
  source: { vmId },
  destination: { public: true },
  transform: [{ headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` } }],
});

The OpenAI, Anthropic, and private Git guides cover the client-side certificate trust each stack needs.

Other Workflows

WorkflowUse inside the VM
Gitvm.exec("git ...") for clone, status, diff, commit, and other Git commands; use a PTY for long transfers
Many forksSnapshot once, then create multiple VMs from that snapshot
Resource usagevm.data().cpuTimeSeconds for CPU time; free -b and df -B1 through exec for current guest memory and disk usage
Object storageMount a bucket with standard Linux tools
MCP servers or notebooksRun the server or Jupyter in the VM and route a domain to it

For lifecycle state, read vm.data() or vms.list({ state }). For a fresh boot, stop the VM and start it again; pause() preserves memory.

Verify Before Cutting Over

Run your application’s existing integration tests against a workspace from your snapshot. Check command exit handling, PTY reattachment, file ownership, pause/resume, and preview authorization with WebSocket traffic. Test scoped clients with only their identity token. Then route new workspaces to Freestyle and drain the existing E2B workspaces.

esc