Skip to content
Go To Dashboard

Compute

Compute gives a deployed agent a separate, isolated, ephemeral Linux environment for work that should not run inside the agent step itself. Agent steps call the typed ctx.sapiom.sandboxes capability; Sapiom supplies the authenticated cloud connection when the deployed agent runs.

What the agent needsRecommended operation
Run one finite commandsandboxes.create(...), sandbox.exec(...), then sandbox.destroy() in finally
Read output as the command runssandbox.execStream(...) and consume its complete output iterable
Start now and inspect the process latersandbox.exec(..., { waitForCompletion: false }), then getProcess or waitForProcess
Inspect or reconnect to an existing sandboxsandboxes.get(...) or list(), then sandboxes.attach(...) for a live handle
Expose a short-lived web appDeclare its port at creation, then call sandbox.deployPreview(...)

Use a new sandbox for isolated, disposable work. Reconnect only when another step deliberately kept a sandbox alive and passed its name forward.

Give each execution attempt its own valid sandbox name, set a short TTL as a cleanup backstop, and still destroy the sandbox explicitly:

const suffix =
`${ctx.executionId}-${ctx.attempts}`
.toLowerCase()
.replace(/[^a-z0-9]/g, "")
.slice(-24) || "local0";
const sandbox = await ctx.sapiom.sandboxes.create({
name: `transform-${suffix}`,
tier: "xs",
ttl: "15m",
});
try {
const result = await sandbox.exec(
`node -e 'const rows=JSON.parse(process.env.ROWS_JSON);process.stdout.write(JSON.stringify({count:rows.length,total:rows.reduce((sum,row)=>sum+row.value,0)}))'`,
{
env: { ROWS_JSON: JSON.stringify(input.rows) },
timeout: 30_000,
processTimeout: 25,
},
);
if (result.exitCode !== 0) {
throw new Error(`Transform exited ${result.exitCode}: ${result.stderr}`);
}
return JSON.parse(result.stdout) as { count: number; total: number };
} finally {
await sandbox.destroy().catch((error) => {
ctx.logger.warn("sandbox cleanup failed", {
sandbox: sandbox.name,
error: String(error),
});
});
}

Sandbox names must be 2–63 lowercase alphanumeric characters or hyphens, with an alphanumeric first and last character, and must be unique within the tenant. Including ctx.attempts prevents a retried step from trying to create the same still-live sandbox again.

A non-zero command exit does not reject sandbox.exec(). It resolves with the process pid, exitCode, stdout, and stderr, so branch on exitCode. Capability, authentication, and polling failures reject the call instead.

timeout limits how long the SDK waits and polls. A wait timeout does not terminate the process. Set processTimeout in seconds when the process itself must be stopped after a deadline, then keep the SDK’s millisecond timeout slightly longer.

Use execStream when logs should reach the agent as they arrive:

const process = await sandbox.execStream("npm test", {
cwd: "app",
});
for await (const line of process.output) {
ctx.logger.info("sandbox output", {
stream: line.stream,
data: line.data,
});
}
if (process.exitCode !== 0) {
throw new Error(`Tests exited ${process.exitCode}`);
}

The exitCode on a streaming result is final only after the output iterable has drained. If the step does not need live lines, start a process with waitForCompletion: false; the immediate result has exitCode: -1, and its pid can be passed to getProcess or waitForProcess on the same sandbox handle.

Sandbox storage disappears with the sandbox. Move any result that must survive to File Storage, Data, or a Repository before cleanup.

Pass the sandbox name between steps, not the live handle. get and list return read-only metadata, not a live handle. Their recorded status is not an active process or URL health probe.

const info = await ctx.sapiom.sandboxes.get(input.sandboxName);
if (info.status !== "running") {
throw new Error(`Sandbox is ${info.status}`);
}
const sandbox = ctx.sapiom.sandboxes.attach(info.name, {
workspaceRoot: info.workspaceRoot,
});

attach does not create a sandbox or check that one is ready. Its first remote operation can still fail if the sandbox expired, was destroyed, or is unhealthy. Supplying the recorded workspaceRoot matters when the attached handle will use filesystem paths or an explicit cwd.

Declare the listening port when the sandbox is created, then use deployPreview to build, start, and expose the app. A Sapiom repository is the safer source when the files need to outlive any one sandbox:

const suffix =
`${ctx.executionId}-${ctx.attempts}`
.toLowerCase()
.replace(/[^a-z0-9]/g, "")
.slice(-24) || "local0";
const host = await ctx.sapiom.sandboxes.create({
name: `preview-${suffix}`,
tier: "xs",
ttl: "30m",
port: 3000,
});
let keepUntilTtl = false;
try {
const preview = await host.deployPreview({
source: { kind: "git", repo: input.repoSlug, ref: input.ref },
build: "npm ci",
start: "npm start",
port: 3000,
});
if (preview.status !== "deployed" || !preview.url) {
throw new Error(
`Preview ${preview.status}: ${preview.logs.slice(-1_000)}`,
);
}
keepUntilTtl = true;
return { url: preview.url, sandboxName: host.name };
} finally {
if (!keepUntilTtl) await host.destroy().catch(() => {});
}

deployPreview can return "failed" with logs instead of throwing; "unverified" means the process started but the public endpoint did not become ready during the check. Infrastructure failures can still reject the call.

createPublicUrl({ port }) is the lower-level option when the agent already started and manages the process itself. The port must still have been declared at sandbox creation, and the method defaults to a public endpoint unless public: false is supplied.

Local Run creates a method-capable fake handle for ctx.sapiom.sandboxes; Local Run does not create a sandbox or execute the command string. Its built-in sandbox.exec result succeeds with empty output and destroy is a no-op. It does not model process state, isolation, installed programs, egress, or an exposed port.

Override the exact capability paths when the step consumes command output:

{
"version": 1,
"steps": {
"transform": {
"sandboxes.create": {
"name": "transform-local0",
"workspaceRoot": "/workspace"
},
"sandbox.exec": {
"pid": "stub-transform",
"exitCode": 0,
"stdout": "{\"count\":2,\"total\":9}",
"stderr": ""
}
}
}
}

Namespace calls use plural paths such as sandboxes.create; methods on the returned handle use singular paths such as sandbox.exec. Advanced handle methods such as execStream, waitForProcess, and deployPreview have no meaningful built-in simulation, so supply the result your branch consumes or put the provider-facing logic behind an injectable adapter.

After the run, assert the intended terminal output and require both unusedStubs and stubWarnings to be empty. Then use a small production run to verify command behavior, network access, preview readiness, and the cleanup path.

A created sandbox can consume metered compute until destroy() succeeds or its TTL elapses. Choose the smallest sufficient tier, use a short TTL as a backstop, and destroy throwaway sandboxes in finally. A TTL is not a substitute for cleanup.

The sandbox does not automatically receive the agent’s secrets. Pass only the specific environment values the workload needs, never put secret values in source, inputs, returned output, or logs, and remember that a public preview can expose anything the server returns.

Use the signed-in capability catalog for current tier availability, limits, and pricing. Do not copy a price into agent logic; usage depends on the sandbox’s selected tier and lifetime.