OpenWork on Differently
OpenWork’s sandbox jobs are written against one provider contract,
@openwork/sandbox.
The Differently provider (@openwork/sandbox-differently) implements that
contract against this API. OpenWork then holds a single Differently key.
Differently holds the OpenSandbox, Daytona and Freestyle credentials, and
decides by policy where each computer runs.
Status: run end to end on 2026-10-29. OpenWork’s headless runner, run
locally, drove Workbot computers through an install on Azure VMs
(azure-vms.md): a model turn used the computer on a
Fast Sandbox microVM, Workbot opened a page in the desktop’s browser, the
viewer streamed it, and OpenWork’s proof:live passed through the gateway,
pause and resume included. The runner fell back to its own provider when the
gateway could not start a computer.
What OpenWork needs, and where Differently provides it
Section titled “What OpenWork needs, and where Differently provides it”| OpenWork contract | Differently | Notes |
|---|---|---|
idempotencyKey names one live instance |
Idempotency-Key header, plus the label openwork.sandbox.key |
A key whose sandbox has ended creates a new one. A replay of a live key is reported to OpenWork as conflict. |
labels, find, list |
labels; GET /v1/sandboxes?label=k=v |
|
image |
a template, or a snapshot ID to boot from | |
resources |
resources (cpu, memoryMiB, diskGiB) |
|
create-time env |
env |
|
lifecycle.autoDeleteMinutes |
ttlSeconds, extended each time the computer resumes |
PATCH ttlSeconds also moves OpenSandbox’s own expiry. |
lifecycle.autoStopMinutes, autoPauseMinutes |
not provided | Workbot pauses idle computers itself. |
run (exact stdout, stderr, exit code) |
POST /v1/sandboxes/{id}/exec |
Exact on OpenSandbox with execd release-1.2.0 and lineEndings: kept. |
files read, write, stat |
files/content GET and PUT; directory listing |
The adapter derives stat from the parent listing and applies mode with chmod. |
pause, start |
/pause, /resume (or /stop, /start) |
|
snapshots |
POST /v1/sandboxes/{id}/snapshots, DELETE /v1/snapshots/{id} |
A snapshot boots only on the pool that took it. |
endpoint(port) |
POST /v1/sandboxes/{id}/ports |
Depends on the backend (see the backend matrix). |
| storage volumes, detached exec | not yet | OpenWork Cloud workers (Den) need both, so they stay on their Daytona provider. Workbot computers do not. |
Errors map onto OpenWork’s taxonomy by code, never by message:
| Differently code | OpenWork code |
|---|---|
not_found |
not_found |
conflict |
conflict |
invalid_state, unsupported_operation, unsupported_capability, invalid_request |
invalid_state (not retried) |
quota_exceeded, no_eligible_pool |
capacity |
rate_limited |
rate_limited |
unauthenticated, permission_denied, suspended |
auth |
provider_unavailable, HTTP 5xx |
transient |
1. Deploy Differently with the pools OpenWork may use
Section titled “1. Deploy Differently with the pools OpenWork may use”Follow deploy/kubernetes.md. For OpenWork:
- OpenSandbox (private). Run its server with
execd_image = "opensandbox/execd:release-1.2.0"or later, and setopensandbox.lineEndings: keptin the chart. With execd 1.1, the output of commands that end without a newline gains one, and OpenWork’s exactruncontract fails. - Daytona or Freestyle (external, optional). Enable
providers.daytonaorproviders.freestylewithexistingSecret, or add them at runtime with their key stored in Differently (keys and providers). Their API keys live in Kubernetes Secrets that only the Differently server reads, and never reach OpenWork. See hybrid-overflow.md. - Policy. Registering a provider authorizes nothing. To route work there,
set
config.policy.externaltooverflow(only when private pools are full) orallowed, and list the pools inconfig.policy.externalPools. A narrower org or project policy can only tighten this.
On OpenSandbox’s Kubernetes runtime (AKS, k3s)
Section titled “On OpenSandbox’s Kubernetes runtime (AKS, k3s)”Observed on a single-node k3s install on Azure (OpenSandbox server, controller and execd release-1.2.0, BatchSandbox): the end-to-end suite passes 8 of 8 with pause and snapshots left out. What that means for OpenWork computers:
- Exact output. Its Kubernetes runtime already uses
execdrelease-1.2.0, so setopensandbox.lineEndings: kept. - Pause and snapshots need a registry. On this runtime, OpenSandbox
pauses by committing the sandbox’s filesystem to an OCI image and resumes
by re-creating it from that image (
controller.snapshot.registryplus push and pull secrets). That is slower than Docker’s memory freeze, and processes don’t survive it. Without a registry, leavelifecycle.pause-resumeandsnapshots.diskout ofopensandbox.features. Workbot then can’t pause idle computers: the pause fails quietly and the computer runs until its TTL. - Port URLs need OpenSandbox’s ingress gateway (
server.gateway.enabled), so a desktop viewer needs it, or a Daytona or Freestyle pool. - First image pull. Pulling a new image can take longer than OpenSandbox’s 60-second pod-ready timeout. OpenSandbox then deletes the sandbox and answers the create with a 504. Pre-pull large images, such as Workbot’s computer image, on the nodes, or raise that timeout (OpenSandbox pools).
Templates: route one image name to every provider
Section titled “Templates: route one image name to every provider”Workbot boots a prepared image, and each provider names its own copy
differently (the runner’s SANDBOX_GATEWAY_TEMPLATE names the template). A Differently template maps one
name to each pool’s image, so the same computer can be placed on any of them:
config: templates: - name: workbot-computer description: Workbot's Linux computer pools: [opensandbox-microvm, freestyle] images: # keyed by pool name opensandbox-microvm: tpl-... # a Fast Sandbox template ID (the pool's templateId) freestyle: workbot-desktop-freestyle-0123456789abcdef # the Freestyle snapshot's slug # opensandbox: <registry>/workbot-computer:<tag> # an image, on the container runtimes # daytona: <snapshot name>A template with images runs only on the pools it lists, and placement
explanations say so. A template name with no configured entry is passed to
the backend unchanged, as before. Freestyle’s VM create takes the snapshot’s
slug (the name OpenWork’s snapshot builder prints), not its sh-... id.
Workbot’s desktop needs Daytona’s linux-vm class, so leave Daytona out of
the template where the account has none.
2. Give OpenWork a project and a key
Section titled “2. Give OpenWork a project and a key”differently admin org create openwork --default-project workbotdifferently key create workbot-runner --org <org-id> --key-project <project-id>The key holds the default sandboxes:read, sandboxes:write and
sandboxes:exec scopes and works only in that project. Set project quotas
(maxActiveSandboxes, CPU, memory) to cap what the runner can hold.
To replace the runner’s key without downtime, rotate it with a grace period
(differently key rotate <key-id> --grace 1h), set the new token as the
runner’s SANDBOX_GATEWAY_API_KEY, and let the old key lapse
(rotating API keys).
Provider keys can be stored in Differently, and which provider new computers
use can be switched there (differently admin pool use NAME), with no change
to OpenWork (keys and providers).
3. Turn it on in OpenWork
Section titled “3. Turn it on in OpenWork”Differently is a sandbox gateway for Workbot, configured on the runner
apart from its own provider (HEADLESS_COMPUTER, which stays Freestyle or
Daytona). This describes OpenWork’s
pull request #6649;
check it for the current state before relying on the details.
SANDBOX_GATEWAY=differentlySANDBOX_GATEWAY_URL=https://differently.example.com # HTTPS onlySANDBOX_GATEWAY_API_KEY=<the project key>SANDBOX_GATEWAY_TEMPLATE=workbot-computer # the Differently templateThen turn on the sandboxGateway feature flag for the organizations that
should use it; every other organization keeps the runner’s own provider.
-
Health decides, per new computer. A flagged conversation’s computer goes through Differently when
GET /v1/capabilitiesanswers with an enabled, healthy pool (checked at most every 30 s). If Differently is unreachable, times out, answers 5xx or cannot start the sandbox, the computer is made on the runner’s own provider instead. -
Refusals are not routed around.
quota_exceeded,no_eligible_pool, permission and invalid or unsupported requests fail the call, so the policies in this project still hold. -
Computers stay where they were made. One made through Differently errors while Differently is down; one made on the fallback is kept until it is gone, and the next computer goes through Differently again. The runner records which side made each computer.
-
Size and pause. Computers ask for their own size, so the project’s quotas must allow it. The runner pauses idle computers only when every pool its key may use can pause (or every one can stop), which it reads from
GET /v1/capabilities. That answer lists only the pools the key’s placement policies allow, so give OpenWork’s organization a policy that allows just the pools Workbot uses, each with pause and snapshots (on the Kubernetes runtime, a snapshot registry):Terminal window echo '{"allowedPools":["opensandbox-microvm","freestyle"]}' > workbot.jsondifferently admin policy org:<org-id> workbot.json
Limits to know
Section titled “Limits to know”- Idle deletion is built from TTL extensions: a computer expires
HEADLESS_COMPUTER_KEEP_DAYSafter it was last resumed, not after its last command. Differently has no idle-based lifecycle of its own yet. - Backstops. On Daytona and Freestyle, a TTL change does not yet move their own deletion deadline (backlog E0.20). Keep their TTLs generous or unset there.
- Desktop viewers need a pool whose backend exposes ports without the backend’s API key: Freestyle (public TLS host), Daytona (signed preview URL), or OpenSandbox behind its ingress gateway (signed URLs on the Kubernetes runtime, route scopes on Fast Sandbox). OpenSandbox’s proxied endpoints are not handed out.
- OpenSandbox’s maximum timeout. The adapter sends a computer’s TTL as
OpenSandbox’s own expiry, and the runner keeps computers
HEADLESS_COMPUTER_KEEP_DAYS(14 by default). Set the OpenSandbox servers’max_sandbox_timeout_secondsabove that, or every create is refused and the runner falls back to its own provider. - Fast Sandbox pause takes minutes (a full checkpoint: about three for a 4 GiB desktop). Differently finishes it even when the runner stops waiting; OpenWork’s runner gives pauses five minutes.
- Den’s Cloud workers need detached processes and persistent volumes; both are in the backlog and Den stays on Daytona until then.
Source: docs/guides/openwork.md