Create & Execute
The default runtime loop for agents and jobs, create, execute, destroy.
Create a sandbox, run commands inside it, then destroy it. This is the default runtime loop for agents and short jobs.
Use this pattern for one-shot tasks and short workflows. For anything that needs to outlive a single request, see Lifecycle and hibernation.
Quick example
import { Isorun } from 'isorun'
const isorun = new Isorun()
const sandbox = await isorun.create({ image: 'python:3.12-slim' })
try {
const result = await sandbox.exec("python3 -c 'print(2**100)'")
console.log(result.stdout.trim())
console.log(result.exitCode)
} finally {
await sandbox.destroy()
}Streaming output while a command runs
sandbox.exec() buffers: it resolves once the command exits, so a long build
prints nothing until the end. To watch output live, call the streaming endpoint
directly — POST /v1/runs/{id}/exec/stream returns Server-Sent Events, one
JSON frame per data: line.
| Approach | Best for | Tradeoff |
|---|---|---|
sandbox.exec(cmd, timeoutSec) | Short bounded commands | No incremental output while running |
POST /v1/runs/{id}/exec/stream | Long builds / tests / training | A few lines of parsing; you handle the frames yourself |
Each frame is { "type": "stdout" \| "stderr" \| "exit", "data": "..." }:
const res = await fetch(`${isorun.apiUrl}/v1/runs/${sandbox.id}/exec/stream`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ISORUN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ command: 'python3 long_training_loop.py', timeout: 600 }),
})
for await (const chunk of res.body) {
for (const line of Buffer.from(chunk).toString().split('\n')) {
if (!line.startsWith('data: ')) continue
const frame = JSON.parse(line.slice(6))
if (frame.type === 'stdout') process.stdout.write(frame.data)
if (frame.type === 'stderr') process.stderr.write(frame.data)
}
}GET /v1/runs/{id}/exec/ws carries the identical frames over a WebSocket if
you would rather not parse SSE.
Handle failures
A non-zero exit code is a normal result, not an exception, check exitCode yourself.
const result = await sandbox.exec('pytest -q', 180)
if (result.exitCode !== 0) {
console.error(result.stderr)
throw new Error('tests failed')
}Clean up and control cost
Always destroy or close sandboxes in finally paths.
- Prevents leaked runtime costs.
- Keeps capacity available under load.
- Makes behavior predictable in production.
A sandbox you forget to destroy keeps billing until its idle timeout fires. Put destroy() in a finally block so it runs even when the command throws.
Next steps
- Lifecycle and hibernation, pause a sandbox without paying for idle time.
- Checkpoints and rollback, snapshot expensive setup and fork it.
- TypeScript SDK, full method reference.