runtime_id, and kill when finished. That is the Gravix equivalent of running many Claude sessions side by side.
| Need | Approach |
|---|---|
| Several independent tasks | Create one sandbox per task (examples below) |
| Long session you step away from | pause / resume — filesystem (and Claude session files) persist |
| Recurring work | Cron or CI creates a sandbox, runs claude -p, kills it |
Prerequisites
export GRAVIXLAYER_API_KEY="your-gravixlayer-api-key"
export ANTHROPIC_API_KEY="sk-ant-..."
export ANTHROPIC_PROVIDER_ID="..." # from get-started
export CLAUDE_NETWORK_POLICY_ID="..."
claude.
Fan out three jobs
Each task gets its own sandbox. Results print as they finish.import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from gravixlayer import GravixLayer
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
PROVIDERS = [os.environ["ANTHROPIC_PROVIDER_ID"]]
POLICIES = [os.environ["CLAUDE_NETWORK_POLICY_ID"]]
KEY = os.environ["ANTHROPIC_API_KEY"]
TASKS = [
"Write /workspace/a/health.py — a FastAPI app with GET /health returning {\"status\":\"ok\"}. Stop when done.",
"Write /workspace/b/server.js — an Express app with GET /health returning {\"status\":\"ok\"}. Stop when done.",
"Write /workspace/c/hello.py that prints hello and exits. Stop when done.",
]
def run_task(prompt: str) -> dict:
sandbox = client.runtime.create(
template="claude",
providers=PROVIDERS,
network_policy_ids=POLICIES,
env_vars={"ANTHROPIC_API_KEY": KEY},
timeout=1800,
metadata={"task": prompt[:60]},
)
try:
result = sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"-p",
prompt,
"--output-format",
"text",
],
timeout=1200,
)
return {
"runtime_id": sandbox.runtime_id,
"exit_code": result.exit_code,
"stdout": (result.stdout or "")[:1500],
}
finally:
sandbox.kill()
with ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(run_task, t) for t in TASKS]
for fut in as_completed(futures):
print(fut.result())
import { GravixLayer } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const providers = [process.env['ANTHROPIC_PROVIDER_ID']!];
const policies = [process.env['CLAUDE_NETWORK_POLICY_ID']!];
const key = process.env['ANTHROPIC_API_KEY']!;
const tasks = [
'Write /workspace/a/health.py — a FastAPI app with GET /health returning {"status":"ok"}. Stop when done.',
'Write /workspace/b/server.js — an Express app with GET /health returning {"status":"ok"}. Stop when done.',
'Write /workspace/c/hello.py that prints hello and exits. Stop when done.',
];
async function runTask(prompt: string) {
const sandbox = await client.runtime.create({
template: 'claude',
providers,
networkPolicyIds: policies,
envVars: { ANTHROPIC_API_KEY: key },
timeout: 1800,
metadata: { task: prompt.slice(0, 60) },
});
try {
const result = await sandbox.runCmd('claude', {
args: ['--dangerously-skip-permissions', '-p', prompt, '--output-format', 'text'],
timeout: 1200,
});
return {
runtimeId: sandbox.runtimeId,
exitCode: result.exitCode,
stdout: (result.stdout || '').slice(0, 1500),
};
} finally {
await sandbox.kill();
}
}
const results = await Promise.all(tasks.map((t) => runTask(t)));
for (const row of results) console.log(row);
for i in a b c; do
RT=$(gravixlayer runtime create \
--template claude \
--provider "$ANTHROPIC_PROVIDER_ID" \
--network-policy "$CLAUDE_NETWORK_POLICY_ID" \
--env "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
--timeout 1800 \
--wait \
--output json | jq -r '.runtime_id')
echo "started $RT ($i)"
gravixlayer runtime exec "$RT" --timeout 1200 -- \
claude --dangerously-skip-permissions \
-p "Write a short note to /workspace/out-$i.txt describing task $i, then stop." \
--output-format text
gravixlayer runtime kill "$RT" -y
done
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
for i in a b c; do
RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
-d "{
\"template\": \"claude\",
\"cloud\": \"aws\",
\"region\": \"us-east-1\",
\"timeout\": 1800,
\"providers\": [\"$ANTHROPIC_PROVIDER_ID\"],
\"network_policy_ids\": [\"$CLAUDE_NETWORK_POLICY_ID\"],
\"env_vars\": {\"ANTHROPIC_API_KEY\": \"$ANTHROPIC_API_KEY\"}
}" | jq -r '.runtime_id')
echo "started $RT ($i)"
# Wait until status is running, then (timeout is milliseconds):
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d "{
\"command\": \"claude\",
\"args\": [
\"--dangerously-skip-permissions\",
\"-p\", \"Write a short note to /workspace/out-$i.txt describing task $i, then stop.\",
\"--output-format\", \"text\"
],
\"timeout\": 1200000
}" | jq .
curl -sS -X DELETE "$API/agents/runtime/$RT" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
done
runtime kill / sandbox.kill() and run:
gravixlayer runtime shell "$RT"
Pause and resume a long session
Useful when a job should keep its/workspace (and Claude --resume state) overnight without burning idle compute.
import json
import os
import time
from gravixlayer.types.runtime import Runtime
sandbox = Runtime.create(
template="claude",
providers=[os.environ["ANTHROPIC_PROVIDER_ID"]],
network_policy_ids=[os.environ["CLAUDE_NETWORK_POLICY_ID"]],
env_vars={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
timeout=0, # no idle timeout
)
print("runtime_id:", sandbox.runtime_id)
sandbox.file.write("/workspace/NOTES.md", "# Notes\n\n- sandbox started\n")
first = sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"--output-format",
"json",
"-p",
"Append three short bullets about /workspace to /workspace/NOTES.md, then stop.",
],
timeout=1800,
)
session_id = json.loads(first.stdout).get("session_id")
print("session_id:", session_id)
sandbox.pause()
print("paused — filesystem kept")
# Later (same process or a new one):
time.sleep(2)
sandbox = Runtime.connect(sandbox.runtime_id)
sandbox.resume()
if session_id:
sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"--resume",
session_id,
"-p",
"Append one more bullet to /workspace/NOTES.md about resuming after pause.",
],
timeout=900,
)
print(sandbox.file.read("/workspace/NOTES.md").content)
sandbox.kill()
import { GravixLayer } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
// create({ timeout }) is a positive number of seconds. Python create(timeout=0) leaves the server default.
let sandbox = await client.runtime.create({
template: 'claude',
providers: [process.env['ANTHROPIC_PROVIDER_ID']!],
networkPolicyIds: [process.env['CLAUDE_NETWORK_POLICY_ID']!],
envVars: { ANTHROPIC_API_KEY: process.env['ANTHROPIC_API_KEY']! },
timeout: 86_400,
});
console.log('runtime_id:', sandbox.runtimeId);
await sandbox.file.write('/workspace/NOTES.md', '# Notes\n\n- sandbox started\n');
const first = await sandbox.runCmd('claude', {
args: [
'--dangerously-skip-permissions',
'--output-format',
'json',
'-p',
'Append three short bullets about /workspace to /workspace/NOTES.md, then stop.',
],
timeout: 1800,
});
const sessionId = JSON.parse(first.stdout).session_id as string | undefined;
console.log('session_id:', sessionId);
await sandbox.pause();
console.log('paused — filesystem kept');
await new Promise((resolve) => setTimeout(resolve, 2000));
sandbox = await client.runtime.get(sandbox.runtimeId);
await sandbox.resume();
if (sessionId) {
await sandbox.runCmd('claude', {
args: [
'--dangerously-skip-permissions',
'--resume',
sessionId,
'-p',
'Append one more bullet to /workspace/NOTES.md about resuming after pause.',
],
timeout: 900,
});
}
console.log((await sandbox.file.read('/workspace/NOTES.md')).content);
await sandbox.kill();
RT=$(gravixlayer runtime create \
--template claude \
--provider "$ANTHROPIC_PROVIDER_ID" \
--network-policy "$CLAUDE_NETWORK_POLICY_ID" \
--env "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
--timeout 0 \
--wait \
--output json | jq -r '.runtime_id')
echo '# Notes' | gravixlayer runtime files write "$RT" /workspace/NOTES.md
SESSION=$(gravixlayer runtime exec "$RT" --timeout 1800 --output json -- \
claude --dangerously-skip-permissions --output-format json \
-p 'Append three short bullets about /workspace to /workspace/NOTES.md, then stop.' \
| jq -r '.stdout' | jq -r '.session_id')
gravixlayer runtime pause "$RT"
# Later:
gravixlayer runtime resume "$RT"
gravixlayer runtime exec "$RT" --timeout 900 -- \
claude --dangerously-skip-permissions --resume "$SESSION" \
-p 'Append one more bullet to /workspace/NOTES.md about resuming after pause.'
gravixlayer runtime files cat "$RT" /workspace/NOTES.md
gravixlayer runtime kill "$RT" -y
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
-d "{
\"template\": \"claude\",
\"cloud\": \"aws\",
\"region\": \"us-east-1\",
\"timeout\": 0,
\"providers\": [\"$ANTHROPIC_PROVIDER_ID\"],
\"network_policy_ids\": [\"$CLAUDE_NETWORK_POLICY_ID\"],
\"env_vars\": {\"ANTHROPIC_API_KEY\": \"$ANTHROPIC_API_KEY\"}
}" | jq -r '.runtime_id')
curl -sS -X POST "$API/agents/runtime/$RT/files/write" "${AUTH[@]}" \
-d '{"path": "/workspace/NOTES.md", "content": "# Notes\n\n- sandbox started\n"}' | jq .
# Wait until running, then:
SESSION=$(curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d '{
"command": "claude",
"args": [
"--dangerously-skip-permissions",
"--output-format", "json",
"-p", "Append three short bullets about /workspace to /workspace/NOTES.md, then stop."
],
"timeout": 1800000
}' | jq -r '.stdout' | jq -r '.session_id')
curl -sS -X POST "$API/agents/runtime/$RT/pause" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" | jq .
# Later:
curl -sS -X POST "$API/agents/runtime/$RT/resume" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" | jq .
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d "{
\"command\": \"claude\",
\"args\": [
\"--dangerously-skip-permissions\",
\"--resume\", \"$SESSION\",
\"-p\", \"Append one more bullet to /workspace/NOTES.md about resuming after pause.\"
],
\"timeout\": 900000
}" | jq .
curl -sS -X DELETE "$API/agents/runtime/$RT" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
Cron: nightly briefing
Drop this next to a.env that exports the variables from Prerequisites. Cron creates a fresh sandbox each run.
#!/usr/bin/env python3
"""nightly_briefing.py — summarize a seeded workspace and exit."""
import os
import sys
from gravixlayer import GravixLayer
def main() -> int:
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
sandbox = client.runtime.create(
template="claude",
providers=[os.environ["ANTHROPIC_PROVIDER_ID"]],
network_policy_ids=[os.environ["CLAUDE_NETWORK_POLICY_ID"]],
env_vars={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
timeout=1800,
metadata={"job": "nightly_briefing"},
)
try:
sandbox.file.write(
"/workspace/inbox.md",
os.environ.get(
"BRIEFING_INPUT",
"# Inbox\n\n- CI green on main\n- 2 open PRs need review\n- Disk usage 72%\n",
),
)
result = sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"-p",
"Read /workspace/inbox.md and write a five-bullet briefing to /workspace/briefing.md.",
"--output-format",
"text",
],
timeout=1200,
)
print(result.stdout)
if not result.success:
print(result.stderr, file=sys.stderr)
return result.exit_code or 1
print(sandbox.file.read("/workspace/briefing.md").content)
return 0
finally:
sandbox.kill()
if __name__ == "__main__":
raise SystemExit(main())
import { GravixLayer } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const sandbox = await client.runtime.create({
template: 'claude',
providers: [process.env['ANTHROPIC_PROVIDER_ID']!],
networkPolicyIds: [process.env['CLAUDE_NETWORK_POLICY_ID']!],
envVars: { ANTHROPIC_API_KEY: process.env['ANTHROPIC_API_KEY']! },
timeout: 1800,
metadata: { job: 'nightly_briefing' },
});
try {
await sandbox.file.write(
'/workspace/inbox.md',
process.env['BRIEFING_INPUT'] ??
'# Inbox\n\n- CI green on main\n- 2 open PRs need review\n- Disk usage 72%\n',
);
const result = await sandbox.runCmd('claude', {
args: [
'--dangerously-skip-permissions',
'-p',
'Read /workspace/inbox.md and write a five-bullet briefing to /workspace/briefing.md.',
'--output-format',
'text',
],
timeout: 1200,
});
console.log(result.stdout);
if (!result.success) {
console.error(result.stderr);
process.exit(result.exitCode || 1);
}
console.log((await sandbox.file.read('/workspace/briefing.md')).content);
} finally {
await sandbox.kill();
}
# crontab example (weekdays 09:00):
# 0 9 * * 1-5 /usr/bin/env bash -lc 'cd /opt/jobs && set -a && source .env && set +a && ./nightly_briefing.sh'
RT=$(gravixlayer runtime create \
--template claude \
--provider "$ANTHROPIC_PROVIDER_ID" \
--network-policy "$CLAUDE_NETWORK_POLICY_ID" \
--env "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
--timeout 1800 \
--wait \
--output json | jq -r '.runtime_id')
printf '%s\n' "${BRIEFING_INPUT:-# Inbox
- CI green on main
- 2 open PRs need review
- Disk usage 72%
}" | gravixlayer runtime files write "$RT" /workspace/inbox.md
gravixlayer runtime exec "$RT" --timeout 1200 -- \
claude --dangerously-skip-permissions \
-p 'Read /workspace/inbox.md and write a five-bullet briefing to /workspace/briefing.md.' \
--output-format text
gravixlayer runtime files cat "$RT" /workspace/briefing.md
gravixlayer runtime kill "$RT" -y
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
-d "{
\"template\": \"claude\",
\"cloud\": \"aws\",
\"region\": \"us-east-1\",
\"timeout\": 1800,
\"providers\": [\"$ANTHROPIC_PROVIDER_ID\"],
\"network_policy_ids\": [\"$CLAUDE_NETWORK_POLICY_ID\"],
\"env_vars\": {\"ANTHROPIC_API_KEY\": \"$ANTHROPIC_API_KEY\"},
\"metadata\": {\"job\": \"nightly_briefing\"}
}" | jq -r '.runtime_id')
curl -sS -X POST "$API/agents/runtime/$RT/files/write" "${AUTH[@]}" \
-d '{
"path": "/workspace/inbox.md",
"content": "# Inbox\n\n- CI green on main\n- 2 open PRs need review\n- Disk usage 72%\n"
}' | jq .
# Wait until running, then (timeout is milliseconds):
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d '{
"command": "claude",
"args": [
"--dangerously-skip-permissions",
"-p", "Read /workspace/inbox.md and write a five-bullet briefing to /workspace/briefing.md.",
"--output-format", "text"
],
"timeout": 1200000
}' | jq .
curl -sS -X DELETE "$API/agents/runtime/$RT" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
sandbox.git (see Edit a repository) and run claude -p "Review this PR…".
Ops checklist
- Cap concurrent sandboxes (service quotas)
- Tag with
metadata(task,pr_number,owner) - Always
killor settimeout - Log
runtime_id, Claudesession_id, andexit_code