Sandbox API

Project memory and resume

Keep each task's turn history in Sandbox, and start a new sandbox from the last saved workspace.

Two features let work on a task outlive the sandbox it ran in:

NeedUse
Remember what each turn did (meta, summary, trajectory, diff, screenshots)Project memory
Continue a task's files in a new sandbox after the old one stopped, expired, or was destroyedResume in a new sandbox

Both are scoped to X-Sandbox-Tenant-Id + X-Sandbox-Project-Id, like every other Sandbox resource. Nothing is shared across projects.

Project memory

Project memory is a small store of turn records per task, served at /sandbox/memory/v1. Any app that uses Sandbox can write to it and read it back, for example to show a task's history or to brief the agent that runs the next turn.

Both tenancy headers are required; a request without them gets 400.

Write a turn

curl -sS -X PUT \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
  -H "Content-Type: application/json" \
  -d '{
    "meta": {
      "agent_id": "claude-code",
      "model": "claude-sonnet-4-5",
      "job_id": "job-123",
      "sandbox_id": "sbx-abc",
      "commit": "abc1234",
      "exit_code": 0,
      "started_at": "2026-11-04T10:00:00Z",
      "ended_at": "2026-11-04T10:05:00Z",
      "tokens": {"input": 1200, "output": 340},
      "cost": 0.04,
      "claude_session_id": "sess-1"
    },
    "summary_md": "## Turn 1\nAdded the login form and its tests."
  }' \
  "$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42/turns/1"

The call is an upsert. It returns the turn record (task_id, turn, meta, summary_md, artifacts, created_at, updated_at, retain_until). Writing the same turn again replaces its meta and summary and keeps its artifacts.

  • Every meta field is optional, and any extra keys you send are kept. meta is capped at 64 KiB and summary_md at 1 MiB.
  • task_id must match [A-Za-z0-9][A-Za-z0-9._-]{0,127}.
  • turn must be between 0 and 9999. Turns are stored as turns/0001/, turns/0002/, and so on.

Attach artifacts

Send the raw bytes. The Content-Type you send is stored and returned on download.

curl -sS -X PUT \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @trajectory.jsonl \
  "$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42/turns/1/artifacts/trajectory.jsonl"

Typical names are trajectory.jsonl, atif.json, diff.patch, claude-session.jsonl and screenshot-001.png. A name must match [A-Za-z0-9._-]{1,128}; any other name gets 400 (or 404 if it contains /). An artifact can be up to 50 MiB; a larger one gets 413. Uploading an artifact for a turn you have not written yet creates that turn with empty meta.

Read it back

CallReturns
GET /sandbox/memory/v1/tasks?limit=50&cursor=Tasks, most recently updated first: task_id, latest_turn, turn_count, updated_at, retain_until. Send next_cursor back as cursor to get the next page.
GET /sandbox/memory/v1/tasks/{task_id}The task index: every turn with its meta and artifact list. 404 if the task has no memory.
GET /sandbox/memory/v1/tasks/{task_id}/turns/{turn}One turn's meta, summary_md and artifact list
GET /sandbox/memory/v1/tasks/{task_id}/turns/{turn}/artifacts/{name}The artifact bytes, with the stored Content-Type
GET /sandbox/memory/v1/project.mdThe project digest (Markdown)

project.md has one section per task: its id, latest turn, turn count, last update, and the first ~2,000 characters of its newest summary. It is rebuilt from the task catalog every time you read it, so it always matches what has been written. Rewriting an older turn does not replace the newer summary.

Delete a task

curl -sS -X DELETE \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
  "$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42"

This removes every turn, artifact and index entry for the task, and returns 204 even if the task was already gone. A Keycloak writer can delete only tasks it created; project_admin and above can delete any task in the project. Any project writer can add turns to any task.

Retention

Every write sets retain_until to the current time plus SANDBOX_MEMORY_RETENTION_SEC, which defaults to 30 days. So a task is kept for 30 days after its last write. The timestamp is recorded on the index and the task list; deleting after that date is left to storage lifecycle rules.

Permissions

Keycloak callers need the UMA resource sandbox-memory. All roles can read; writers and above can write and delete, and delete is further limited to the task's owner as described above. Platform API keys and team tokens follow the same tenancy rules as the rest of Sandbox.

Resume in a new sandbox

Each time a sandbox that belongs to a task saves a checkpoint (on stop, autosave, expiry, destroy or workspace push), Sandbox records it as that task's latest workspace. A sandbox belongs to a task when it is created with task_id, labels.task_id, labels.ctp_task_id or labels.workspace_key. A new sandbox can then start from that snapshot, even after the old sandbox has been destroyed.

Explicit: source.type = workspace_latest

curl -sS -X POST \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "sandboxes-default",
    "task_id": "task-42",
    "source": {"type": "workspace_latest"}
  }' \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1"

The create response names the snapshot it restored:

{
  "source": {
    "type": "workspace_latest",
    "workspace_key": "task-42",
    "sandbox_id": "8aee8bce-…",
    "snapshot_seq": 3
  }
}

That pinned snapshot is also what the sandbox goes back to if it is restarted before it has saved a checkpoint of its own.

  • To restore a different task's workspace, pass source.workspace_key instead of labelling the sandbox.
  • To restore one exact snapshot, pass source.sandbox_id and source.snapshot_seq.
  • If the task has no snapshot yet (its first turn), the sandbox starts with an empty workspace and source comes back without sandbox_id.
  • If there is no task label and no workspace_key, the create fails with 400.

Automatic, with reuse: restore_latest: true

{
  "profile": "sandboxes-default",
  "task_id": "task-42",
  "reuse": true,
  "restore_latest": true
}

If a running or stopped sandbox matches the reuse fingerprint, it is returned as before. If none matches, the new sandbox starts from the task's latest workspace snapshot. If the task has never saved one, the new sandbox starts empty, exactly as without the flag. restore_latest also works without reuse. It is ignored when you send an explicit source, and it defaults to false, so existing callers see no change.

Which snapshot counts as latest

Normally it is the most recent checkpoint for the task. The exception: once a newer sandbox on the task has saved a checkpoint, an older sandbox can no longer take the pointer back. This matters when the old sandbox is destroyed or expires after its replacement has done new work. Its final checkpoint is saved, but the task still resumes from the replacement's work.

Not for pass@k

Every Pass@K attempt must start from the same clean workspace. Do not use workspace_latest or restore_latest for attempts that must be independent.

Related: Long-lived sandboxes, Project memory API, Sandboxes API.