Piston API

Piston is Motor's sandboxed code runner: Docker images go in, and results come out. Agents, not people, write its configs and read its results, so each error says what to do.

  • Pack the files as a tar.zst, and register it with POST /api/piston/sources. PUT its bytes to upload_url, unless upload_url is null.
  • Check the config, piston.star, with POST /api/piston/plans, if you like.
  • Start a run with POST /api/piston/runs, with an Idempotency-Key.
  • Stream the logs with GET /api/piston/runs/{run_id}/logs/stream, which ends with the run, or get the run until finished_at isn't null.
  • Read the results in the run: each task's status, its steps' exit codes, its test reports, and failure. Download the outputs.

Each request acts in one team: team_id is a query parameter of a GET or a DELETE, and a field of the body of a POST or a PUT. An agent token acts in its own team if you don't say. Sources and runs are billable: a team that can't pay gets a 402. A run that is errored failed because of Piston, not your code: start it again. The conventions of every Motor API are at https://new.motor.dev/docs.

Base URL https://new.motor.dev · version 2026-10-22 · OpenAPI 3.1 spec

Sources

A source is a tar.zst of files, which a run's tasks find in their working directory. Piston keeps it for 30 days.

POST /api/piston/sources

Register a source

Registers a source of the team by its digest: a tar.zst of the files that the tasks find in their working directory. Make it from the files, and send its SHA-256 digest and size. If upload_url isn't null, PUT the tar.zst's bytes to it, with no Motor credential, before upload_expires_at; then start a run with the source's id. If upload_url is null, Piston has the source already. Registering the same digest again is safe, and gives a new upload_url until the upload is done. Needs piston:run for an agent, and a team that can pay.

Body fieldTypeDescription
sha256requiredstring

The SHA-256 digest of the tar.zst, in lowercase hex.

sizerequiredinteger

The size of the tar.zst, in bytes.

repostring

Where the files came from, such as motor/aragorn, for people who read the run.

team_idstring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Example request
{
  "repo": "motor/aragorn",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "size": 1843200,
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM"
}
Responses

200The source, and where to upload it.

FieldTypeDescription
sourceobject
upload_expires_atstring or null

When upload_url stops working. Register the source again for a new URL.

upload_urlstring or null

The URL to PUT the bytes of the tar.zst to, with no Motor credential, before upload_expires_at. Null when Piston has the source already: go on to the run.

{
  "source": {
    "created_at": "2026-10-22T09:00:00.000Z",
    "id": "src_1BbA6sXxyUWHYvKqN5T3dM",
    "repo": "motor/aragorn",
    "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "size": 1843200,
    "uploaded": false
  },
  "upload_expires_at": "2026-10-22T09:15:00.000Z",
  "upload_url": "https://piston-data.s3.amazonaws.com/teams/team_1BbA6sXxyUWHYvKqN5T3dM/sources/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.tar.zst?X-Amz-Signature=…"
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

402The team can't pay for new work: `billing_required` (the team needs a card: an admin adds one in the console) or `over_limit` (the team or the agent spent its spending limit for the month: an admin raises the team's limit, or the person or agent that started the agent starts one with a higher limit). Retrying doesn't help until that changes.

{
  "error": {
    "code": "over_limit",
    "message": "The team spent its spending limit for this month, so Piston doesn't start new work for it. An admin raises the limit at https://new.motor.dev/console, or the work waits until next month.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Plans

A plan is the tasks of a run. A Starlark config in the source, piston.star, evaluates to one, or you send one as JSON.

POST /api/piston/plans

Check a plan

Evaluates the config of an uploaded source (source_id, with path), or checks a plan that you send as JSON (plan), without running it. Use it to find the problems of a config before a run: a config with problems is a 422 invalid_plan with each problem in diagnostics. Send tasks to keep only those tasks and the tasks that they need. Needs piston:run for an agent.

Body fieldTypeDescription
pathstring

The config's path in the source: piston.star if you don't say. Only with source_id.

planobject

A plan to check, as JSON. Send source_id or plan.

source_idstring

The source whose config to evaluate. Send source_id or plan.

tasksarray of string

The tasks to keep, with the tasks that they need. All the tasks if you don't say.

team_idstring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Example request
{
  "path": "piston.star",
  "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
  "tasks": [
    "test"
  ],
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM"
}
Responses

200The plan, as a run would run it.

FieldTypeDescription
diagnosticsarray of object

Warnings, if any. A plan with errors is a 422 invalid_plan instead.

planobject

The tasks of a run, as piston.star evaluates to them. Values reach a step only as environment variables, files, or argv; Piston never puts text into a command.

{
  "diagnostics": [],
  "plan": {
    "tasks": [
      {
        "caches": {
          "/root/go/pkg/mod": "go_mod"
        },
        "cpu": 4,
        "image": "golang:1.26",
        "memory_gb": 16,
        "name": "test",
        "platform": "linux/amd64",
        "steps": [
          {
            "name": "vet",
            "run": "go vet ./..."
          },
          {
            "name": "test",
            "report": {
              "format": "go_test_json",
              "path": "test.json"
            },
            "run": "go test -json ./... \u003e test.json"
          }
        ]
      }
    ]
  }
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422`invalid_plan`: the config or the plan has problems, each in `diagnostics`, with its file, line, column, the path of its field, a stable code, and what to do. Fix all of them, and send the request again. `invalid_field`: a field of the request has a problem, which `field` names.

{
  "error": {
    "code": "invalid_plan",
    "diagnostics": [
      {
        "code": "P0107",
        "column": 9,
        "file": "piston.star",
        "line": 14,
        "message": "\"go mod\" isn't snake case; use \"go_mod\"",
        "path": "tasks.test.caches"
      }
    ],
    "message": "The plan has 1 problem. Fix each one, and send the request again.\npiston.star:14:9: tasks.test.caches: \"go mod\" isn't snake case; use \"go_mod\" [P0107]",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Runs

A run is some tasks of a plan, with the tasks that they need, on one source. Each task runs on its own new instance.

GET /api/piston/runs

List the runs

Returns the team's runs, the newest first, without their tasks and plans. Get a run for its results. Needs piston:read for an agent.

ParameterTypeDescription
team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

limitin queryinteger

The most items in the page, from 1 to 100. 50 if you don't say.

cursorin querystring

The next_cursor of the last page.

Responses

200A page of runs.

FieldTypeDescription
dataarray of object
next_cursorstring or null

The cursor of the next page, for cursor, or null on the last page.

{
  "data": [
    {
      "agent_id": "agent_1BbA6sXxyUWHYvKqN5T3dM",
      "created_at": "2026-10-22T09:00:00.000Z",
      "finished_at": "2026-10-22T09:01:12.500Z",
      "id": "run_1BbA6sXxyUWHYvKqN5T3dM",
      "memory_byte_milliseconds": "1151051235328000",
      "reason": null,
      "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
      "started_at": "2026-10-22T09:00:05.250Z",
      "status": "failed",
      "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM",
      "user_id": "user_1BbA6sXxyUWHYvKqN5T3dM",
      "vcpu_milliseconds": 268000
    }
  ],
  "next_cursor": null
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

POST /api/piston/runs

Start a run

Starts a run of an uploaded source: the tasks of its config (path), or of a plan that you send (plan). Each task runs on its own new instance, in a container of its image, with the source in its working directory, and Piston bills its size from its first step to its end. Send an Idempotency-Key, so that a retry doesn't start a second run. A config with problems is a 422 invalid_plan, with each problem in diagnostics. Then follow the run: stream its logs, or get the run until finished_at isn't null. Needs piston:run for an agent, and a team that can pay.

ParameterTypeDescription
Idempotency-Keyin headerstring

A key, 1 to 128 printable characters, that makes the request happen once: for 24 hours, the same key and request get the first answer again.

Body fieldTypeDescription
source_idrequiredstring

The uploaded source to run on.

pathstring

The config's path in the source: piston.star if you don't say. Send path or plan.

planobject

A plan to run, as JSON, instead of the source's config.

tasksarray of string

The tasks to run, with the tasks that they need. All the tasks if you don't say.

team_idstring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Example request
{
  "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
  "tasks": [
    "test"
  ],
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM"
}
Responses

201The run, queued. `Location` has its URL. A replay of an Idempotency-Key has `Idempotency-Replayed: true`.

FieldTypeDescription
agent_idstring or null

The agent that started the run, or null for a person.

created_atstring

When the run started.

finished_atstring or null

When the last task ended. The run is over once this isn't null.

idstring

The run's ID, run_….

memory_byte_millisecondsstring

The memory that the run's tasks used, which Piston bills, as a string, because it can pass 2^53.

reasonstring or null

Why the run ended when it didn't succeed, such as over_limit, or null.

source_idstring
started_atstring or null

When the first task started.

statusqueued | starting | running | succeeded | failed | canceled | timed_out | errored | skipped

queued (waiting for the tasks that it needs, or for an instance), starting (an instance starts), running, and the final ones: succeeded, failed (a step failed: read failure), canceled, timed_out, errored (Piston failed, not your code: start the run again), and skipped (a task that it needs didn't succeed).

team_idstring
user_idstring or null

The person that started the run, or that the agent works for.

vcpu_millisecondsinteger

The compute that the run's tasks used, which Piston bills.

planobject

The run's plan, with the digest of each image. Not in a list of runs.

tasksarray of object

The run's tasks and their results. Not in a list of runs.

{
  "agent_id": "agent_1BbA6sXxyUWHYvKqN5T3dM",
  "created_at": "2026-10-22T09:00:00.000Z",
  "finished_at": null,
  "id": "run_1BbA6sXxyUWHYvKqN5T3dM",
  "memory_byte_milliseconds": "0",
  "plan": {
    "tasks": [
      {
        "caches": {
          "/root/go/pkg/mod": "go_mod"
        },
        "cpu": 4,
        "image": "golang:1.26@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "memory_gb": 16,
        "name": "test",
        "platform": "linux/amd64",
        "steps": [
          {
            "name": "vet",
            "run": "go vet ./..."
          },
          {
            "name": "test",
            "report": {
              "format": "go_test_json",
              "path": "test.json"
            },
            "run": "go test -json ./... \u003e test.json"
          }
        ]
      }
    ]
  },
  "reason": null,
  "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
  "started_at": null,
  "status": "queued",
  "tasks": [
    {
      "cached_from_run_id": null,
      "cpu": 4,
      "failure": null,
      "finished_at": null,
      "key": null,
      "memory_gb": 16,
      "name": "test",
      "outputs": [],
      "platform": "linux/amd64",
      "reports": [],
      "started_at": null,
      "status": "queued",
      "steps": []
    }
  ],
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM",
  "user_id": "user_1BbA6sXxyUWHYvKqN5T3dM",
  "vcpu_milliseconds": 0
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

402The team can't pay for new work: `billing_required` (the team needs a card: an admin adds one in the console) or `over_limit` (the team or the agent spent its spending limit for the month: an admin raises the team's limit, or the person or agent that started the agent starts one with a higher limit). Retrying doesn't help until that changes.

{
  "error": {
    "code": "over_limit",
    "message": "The team spent its spending limit for this month, so Piston doesn't start new work for it. An admin raises the limit at https://new.motor.dev/console, or the work waits until next month.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422`invalid_plan`: the config or the plan has problems, each in `diagnostics`, with its file, line, column, the path of its field, a stable code, and what to do. Fix all of them, and send the request again. `invalid_field`: a field of the request has a problem, which `field` names.

{
  "error": {
    "code": "invalid_plan",
    "diagnostics": [
      {
        "code": "P0107",
        "column": 9,
        "file": "piston.star",
        "line": 14,
        "message": "\"go mod\" isn't snake case; use \"go_mod\"",
        "path": "tasks.test.caches"
      }
    ],
    "message": "The plan has 1 problem. Fix each one, and send the request again.\npiston.star:14:9: tasks.test.caches: \"go mod\" isn't snake case; use \"go_mod\" [P0107]",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

GET /api/piston/runs/{run_id}

Get a run

Returns a run with its results: each task's status, each step's exit code, the parsed test reports, the outputs, and failure, with the end of the log of the step that failed. The run is over once finished_at isn't null. Needs piston:read for an agent.

ParameterTypeDescription
run_idrequired in pathstring

The run's ID.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

200The run.

FieldTypeDescription
agent_idstring or null

The agent that started the run, or null for a person.

created_atstring

When the run started.

finished_atstring or null

When the last task ended. The run is over once this isn't null.

idstring

The run's ID, run_….

memory_byte_millisecondsstring

The memory that the run's tasks used, which Piston bills, as a string, because it can pass 2^53.

reasonstring or null

Why the run ended when it didn't succeed, such as over_limit, or null.

source_idstring
started_atstring or null

When the first task started.

statusqueued | starting | running | succeeded | failed | canceled | timed_out | errored | skipped

queued (waiting for the tasks that it needs, or for an instance), starting (an instance starts), running, and the final ones: succeeded, failed (a step failed: read failure), canceled, timed_out, errored (Piston failed, not your code: start the run again), and skipped (a task that it needs didn't succeed).

team_idstring
user_idstring or null

The person that started the run, or that the agent works for.

vcpu_millisecondsinteger

The compute that the run's tasks used, which Piston bills.

planobject

The run's plan, with the digest of each image. Not in a list of runs.

tasksarray of object

The run's tasks and their results. Not in a list of runs.

{
  "agent_id": "agent_1BbA6sXxyUWHYvKqN5T3dM",
  "created_at": "2026-10-22T09:00:00.000Z",
  "finished_at": "2026-10-22T09:01:12.500Z",
  "id": "run_1BbA6sXxyUWHYvKqN5T3dM",
  "memory_byte_milliseconds": "1151051235328000",
  "plan": {
    "tasks": [
      {
        "caches": {
          "/root/go/pkg/mod": "go_mod"
        },
        "cpu": 4,
        "image": "golang:1.26@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "memory_gb": 16,
        "name": "test",
        "platform": "linux/amd64",
        "steps": [
          {
            "name": "vet",
            "run": "go vet ./..."
          },
          {
            "name": "test",
            "report": {
              "format": "go_test_json",
              "path": "test.json"
            },
            "run": "go test -json ./... \u003e test.json"
          }
        ]
      }
    ]
  },
  "reason": null,
  "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
  "started_at": "2026-10-22T09:00:05.250Z",
  "status": "failed",
  "tasks": [
    {
      "cached_from_run_id": null,
      "cpu": 4,
      "failure": {
        "log_tail": "--- FAIL: TestNew (0.00s)\n    ids_test.go:12: got \"run_\", want \"src_\"\nFAIL",
        "message": "The step exited with 1.",
        "step": "test"
      },
      "finished_at": "2026-10-22T09:01:12.500Z",
      "key": null,
      "memory_gb": 16,
      "name": "test",
      "outputs": [],
      "platform": "linux/amd64",
      "reports": [
        {
          "failed": 1,
          "failures": [
            {
              "file": "lib/ids/ids_test.go",
              "line": 12,
              "message": "ids_test.go:12: got \"run_\", want \"src_\"",
              "name": "github.com/motorteam/aragorn/lib/ids.TestNew"
            }
          ],
          "format": "go_test_json",
          "passed": 41,
          "skipped": 0,
          "step": "test"
        }
      ],
      "started_at": "2026-10-22T09:00:05.250Z",
      "status": "failed",
      "steps": [
        {
          "exit_code": 0,
          "finished_at": "2026-10-22T09:00:20.000Z",
          "name": "vet",
          "started_at": "2026-10-22T09:00:05.250Z",
          "status": "succeeded"
        },
        {
          "exit_code": 1,
          "finished_at": "2026-10-22T09:01:12.500Z",
          "name": "test",
          "started_at": "2026-10-22T09:00:20.000Z",
          "status": "failed"
        }
      ]
    }
  ],
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM",
  "user_id": "user_1BbA6sXxyUWHYvKqN5T3dM",
  "vcpu_milliseconds": 268000
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

POST /api/piston/runs/{run_id}/cancel

Cancel a run

Cancels the tasks of a run that haven't ended, and terminates their instances. A task that ended keeps its result. Canceling a run that is over changes nothing. Needs piston:run for an agent.

ParameterTypeDescription
run_idrequired in pathstring

The run's ID.

Body fieldTypeDescription
team_idstring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Example request
{
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM"
}
Responses

200The run.

FieldTypeDescription
agent_idstring or null

The agent that started the run, or null for a person.

created_atstring

When the run started.

finished_atstring or null

When the last task ended. The run is over once this isn't null.

idstring

The run's ID, run_….

memory_byte_millisecondsstring

The memory that the run's tasks used, which Piston bills, as a string, because it can pass 2^53.

reasonstring or null

Why the run ended when it didn't succeed, such as over_limit, or null.

source_idstring
started_atstring or null

When the first task started.

statusqueued | starting | running | succeeded | failed | canceled | timed_out | errored | skipped

queued (waiting for the tasks that it needs, or for an instance), starting (an instance starts), running, and the final ones: succeeded, failed (a step failed: read failure), canceled, timed_out, errored (Piston failed, not your code: start the run again), and skipped (a task that it needs didn't succeed).

team_idstring
user_idstring or null

The person that started the run, or that the agent works for.

vcpu_millisecondsinteger

The compute that the run's tasks used, which Piston bills.

planobject

The run's plan, with the digest of each image. Not in a list of runs.

tasksarray of object

The run's tasks and their results. Not in a list of runs.

{
  "agent_id": "agent_1BbA6sXxyUWHYvKqN5T3dM",
  "created_at": "2026-10-22T09:00:00.000Z",
  "finished_at": "2026-10-22T09:01:12.500Z",
  "id": "run_1BbA6sXxyUWHYvKqN5T3dM",
  "memory_byte_milliseconds": "1151051235328000",
  "plan": {
    "tasks": [
      {
        "caches": {
          "/root/go/pkg/mod": "go_mod"
        },
        "cpu": 4,
        "image": "golang:1.26@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "memory_gb": 16,
        "name": "test",
        "platform": "linux/amd64",
        "steps": [
          {
            "name": "vet",
            "run": "go vet ./..."
          },
          {
            "name": "test",
            "report": {
              "format": "go_test_json",
              "path": "test.json"
            },
            "run": "go test -json ./... \u003e test.json"
          }
        ]
      }
    ]
  },
  "reason": "canceled",
  "source_id": "src_1BbA6sXxyUWHYvKqN5T3dM",
  "started_at": "2026-10-22T09:00:05.250Z",
  "status": "canceled",
  "tasks": [
    {
      "cached_from_run_id": null,
      "cpu": 4,
      "failure": {
        "log_tail": "",
        "message": "The run was canceled.",
        "step": "test"
      },
      "finished_at": "2026-10-22T09:01:12.500Z",
      "key": null,
      "memory_gb": 16,
      "name": "test",
      "outputs": [],
      "platform": "linux/amd64",
      "reports": [],
      "started_at": "2026-10-22T09:00:05.250Z",
      "status": "canceled",
      "steps": [
        {
          "exit_code": 0,
          "finished_at": "2026-10-22T09:00:20.000Z",
          "name": "vet",
          "started_at": "2026-10-22T09:00:05.250Z",
          "status": "succeeded"
        },
        {
          "exit_code": 1,
          "finished_at": "2026-10-22T09:01:12.500Z",
          "name": "test",
          "started_at": "2026-10-22T09:00:20.000Z",
          "status": "failed"
        }
      ]
    }
  ],
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM",
  "user_id": "user_1BbA6sXxyUWHYvKqN5T3dM",
  "vcpu_milliseconds": 268000
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Logs

The lines that the steps write, in order, with a cursor for each.

GET /api/piston/runs/{run_id}/logs

Read the logs

Returns a run's log lines after the cursor after, in order. Poll with next_cursor as after until the run is over, or stream instead. Needs piston:read for an agent.

ParameterTypeDescription
run_idrequired in pathstring

The run's ID.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

taskin querystring

Only the lines of this task. The lines of all the tasks if you don't say.

afterin querystring

The cursor of the last line that you have. The start of the log if you don't say.

limitin queryinteger

The most lines in the page, from 1 to 5000. 1000 if you don't say.

Responses

200A page of lines.

FieldTypeDescription
dataarray of object
has_moreboolean

Whether more lines follow now. When it's false and the run isn't over, poll again later, or stream instead.

next_cursorstring or null

The cursor of the last line of the page, for after; the given after when the page is empty; null when the run has no lines.

{
  "data": [
    {
      "at": "2026-10-22T09:00:05.250Z",
      "cursor": "c_000000000000002a",
      "step": 1,
      "stream": "stdout",
      "task": "test",
      "text": "ok  \tgithub.com/motorteam/aragorn/lib/ids\t0.012s"
    }
  ],
  "has_more": false,
  "next_cursor": "c_000000000000002a"
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

GET /api/piston/runs/{run_id}/logs/stream

Stream the logs

Streams a run's log lines as server-sent events while it runs, from after the cursor in Last-Event-ID or after, or from the start. Each line is id: <cursor>, event: line, and data: <the line as JSON>. When the run ends, event: end has the run as JSON, with its results, and the stream ends: don't connect again. event: error has an error body, and ends the stream: connect again with the last cursor if the error is retryable. A comment comes every 25 seconds. A stream ends after an hour; connect again with the cursor of the last line that you got. Needs piston:read for an agent.

ParameterTypeDescription
run_idrequired in pathstring

The run's ID.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

taskin querystring

Only the lines of this task. The lines of all the tasks if you don't say.

afterin querystring

The cursor of the last line that you have.

Last-Event-IDin headerstring

The cursor of the last line that you got; EventSource clients send it when they connect again.

Responses

200The stream.

id: c_000000000000002a
event: line
data: {"task":"test","step":1,"stream":"stdout","text":"ok  \tgithub.com/motorteam/aragorn/lib/ids\t0.012s","at":"2026-10-22T09:00:05.250Z","cursor":"c_000000000000002a"}

event: end
data: {"id":"run_1BbA6sXxyUWHYvKqN5T3dM","status":"succeeded",…}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Outputs

The files and directories that tasks make, which other tasks use as inputs.

GET /api/piston/runs/{run_id}/outputs/{task}/{output}

Download an output

Redirects (302) to a download of an output of a task, which works for 15 minutes. Follow Location without your credential, such as with curl -L. A directory downloads as a tar.zst. A task that reused a cached result has the outputs of that result. Needs piston:read for an agent.

ParameterTypeDescription
run_idrequired in pathstring

The run's ID.

taskrequired in pathstring

The task's name.

outputrequired in pathstring

The output's name in the task.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

302The output. `Location` has the URL of the download.

{
  "output": {
    "directory": false,
    "download_url": "https://new.motor.dev/api/piston/runs/run_1BbA6sXxyUWHYvKqN5T3dM/outputs/build_amd64/motor",
    "name": "motor",
    "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "size": 31457280
  },
  "url": "https://piston-data.s3.amazonaws.com/teams/team_1BbA6sXxyUWHYvKqN5T3dM/blobs/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08?X-Amz-Signature=…"
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Volumes

Named disks of the team, which tasks mount: dependency caches, and Docker's image stores and layer caches.

GET /api/piston/volumes

List the volumes

Returns the team's volumes: the caches of tasks, and the image stores and layer caches of their Docker daemons. A task gets a copy of its volume, and a task that succeeds commits its changes. Needs piston:read for an agent.

ParameterTypeDescription
team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

200The volumes.

FieldTypeDescription
dataarray of object
next_cursorstring or null

Always null: the list has every volume.

{
  "data": [
    {
      "committed_at": "2026-10-22T09:01:12.500Z",
      "committed_by_run_id": "run_1BbA6sXxyUWHYvKqN5T3dM",
      "id": "vol_1BbA6sXxyUWHYvKqN5T3dM",
      "key": "go_mod",
      "size": 734003200,
      "used_at": "2026-10-22T09:01:12.500Z"
    }
  ],
  "next_cursor": null
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

DELETE /api/piston/volumes/{volume_id}

Delete a volume

Deletes a volume, such as a cache that has bad files. The next task with its key starts with an empty volume. Needs piston:run for an agent.

ParameterTypeDescription
volume_idrequired in pathstring

The volume's ID.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

204The volume is deleted.

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

Secrets

Values that tasks get as environment variables, only when their plans name them.

GET /api/piston/secrets

List the secrets

Returns the names of the team's secrets, never their values. A task gets a secret only when its plan names it in secrets. Needs piston:secrets for an agent.

ParameterTypeDescription
team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

200The secrets.

FieldTypeDescription
dataarray of object
next_cursorstring or null

Always null: the list has every secret.

{
  "data": [
    {
      "name": "registry_token",
      "updated_at": "2026-10-22T09:00:00.000Z"
    }
  ],
  "next_cursor": null
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

PUT /api/piston/secrets/{name}

Set a secret

Sets a secret of the team, or replaces its value. Piston encrypts the value, and gives it only to the tasks whose plans name it; nobody reads it back. Needs piston:secrets for an agent.

ParameterTypeDescription
namerequired in pathstring

The secret's name, in snake case, such as registry_token.

Body fieldTypeDescription
valuerequiredstring

The secret's value, at most 64 KiB.

team_idstring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Example request
{
  "team_id": "team_1BbA6sXxyUWHYvKqN5T3dM",
  "value": "ghp_…"
}
Responses

200The secret, without its value.

FieldTypeDescription
namestring

The secret's name, in snake case, for a task's secrets.

updated_atstring

When the value was last set.

{
  "name": "registry_token",
  "updated_at": "2026-10-22T09:00:00.000Z"
}

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}

DELETE /api/piston/secrets/{name}

Delete a secret

Deletes a secret of the team. A run that starts later and names it fails its check. Needs piston:secrets for an agent.

ParameterTypeDescription
namerequired in pathstring

The secret's name, in snake case, such as registry_token.

team_idin querystring

The team's ID. An agent token acts in its own team if you don't say; a session key or a user key must say.

Responses

204The secret is deleted.

401The credential is missing, unknown, revoked, or expired, or a bound session key has no valid DPoP proof.

{
  "error": {
    "code": "unauthorized",
    "message": "The credential is unknown, revoked, or expired.",
    "retryable": false
  }
}

403The credential can't do this: `insufficient_scope` (the agent needs the scope that the message names: ask for an agent with it), `forbidden`, or `team_suspended`.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The agent's scopes don't include piston:run.",
    "retryable": false
  }
}

404There is no such resource in the team, or the credential can't see it. Check the ID, and `team_id`.

{
  "error": {
    "code": "not_found",
    "message": "There is no run run_1BbA6sXxyUWHYvKqN5T3dM in the team.",
    "retryable": false
  }
}

409The state of things doesn't allow the request, such as a run of a source that isn't uploaded yet: the message says what to do. `idempotency_conflict`: the Idempotency-Key was used with another request; use a new key. `request_in_progress`: the first request with the key hasn't ended; retry after `retry_after_seconds`.

{
  "error": {
    "code": "conflict",
    "message": "The source src_1BbA6sXxyUWHYvKqN5T3dM isn't uploaded yet: PUT it to its upload_url first.",
    "retryable": false
  }
}

422A field or a parameter has a value that Motor or Piston can't accept: `field` names it, and the message says what to send.

{
  "error": {
    "code": "invalid_field",
    "field": "sha256",
    "message": "`sha256` must be the SHA-256 digest of the source's tar.zst, as 64 lowercase hex digits.",
    "retryable": false
  }
}

429The credential made more than 600 requests in a minute, or Piston is at its capacity. Wait `retry_after_seconds`, and retry.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests: the limit is 600 a minute.",
    "retry_after_seconds": 12,
    "retryable": true
  }
}

503Piston can't answer now (`temporarily_unavailable`): wait `retry_after_seconds`, and retry the same request, with the same Idempotency-Key. `unavailable`: this Motor isn't set up to call Piston.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Piston can't answer now. Try again in a moment.",
    "retry_after_seconds": 1,
    "retryable": true
  }
}