---
title: "Actor runs · Scrape.ly Docs"
description: "List, inspect, abort, metamorph, resurrect, charge, and read logs and storages of runs."
url: https://scrape.ly/docs/api-runs
markdown: https://scrape.ly/docs/api-runs.md
---

# Actor runs

> List, inspect, abort, metamorph, resurrect, charge, and read logs and storages of runs.

Inspect, update, abort, metamorph, reboot, resurrect, charge, and read logs for Actor runs. All paths are relative to `https://api.scrape.ly`.

To **start** a run, use [`POST /v2/actors/{actorId}/runs`](https://scrape.ly/docs/api-actors.md#start-a-run) or [run a task](https://scrape.ly/docs/api-tasks.md#run-task). The JSON body is the input object at the top level.

Statuses: `READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `ABORTING`, `ABORTED`, `TIMING-OUT`, `TIMED-OUT`. Terminal statuses are `SUCCEEDED`, `FAILED`, `ABORTED`, and `TIMED-OUT`.

Run statuses use a **hyphen**: `TIMED-OUT`. Webhook event names use an **underscore**: `ACTOR.RUN.TIMED_OUT`.

## List runs

```
GET /v2/actor-runs
```

Lists **your** runs across all Actors. Accepts a user API key, an OAuth token, or a full-permission scoped token. Limited scoped tokens get `403 insufficient-permissions`.

| Query param | Description |
| --- | --- |
| `status` | One value or a comma-separated list, for example `RUNNING,SUCCEEDED`. Use `TIMED-OUT`. |
| `startedAfter` / `startedBefore` | ISO 8601 timestamps. |
| `origin` | How the run was started: `API`, `CLI`, `WEB`, `DEVELOPMENT`, or `SCHEDULER`. |
| `search` | Matches the run ID or the Actor name. |
| `sortBy` | `startedAt` (default), `finishedAt`, `status`, `duration`, or `usage`. |
| `limit` / `offset` / `desc` | [Pagination](https://scrape.ly/docs/api-overview.md#pagination). Default and maximum `limit` is `1000`. `desc` defaults to `false`. |

To list the runs of **one** Actor, use [`GET /v2/actors/{actorId}/runs`](https://scrape.ly/docs/api-actors.md#list-actor-runs). It takes the same `status`, `startedAfter`, `startedBefore`, `sortBy`, and pagination parameters. For a task, use [`GET /v2/actor-tasks/{taskId}/runs`](https://scrape.ly/docs/api-tasks.md#list-task-runs).

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-runs?status=RUNNING,SUCCEEDED&limit=5&desc=1"
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs?limit=5&desc=1" }
```

## Get run

```
GET /v2/actor-runs/{runId}
```

You can only read your own runs. A run's scoped token can read its own run and the child runs it started. If the Actor defines an output schema, the response also includes a computed `output` map of URLs. On someone else's Actor, templates that reference build fields (such as `{{run.buildId}}`) resolve to empty strings.

Build info is owner only. On runs of an Actor you don't own, `buildId`, `buildNumber`, and `options.build` are left out, here and everywhere else runs are returned (run lists, task `lastRun`, webhooks, and the dashboard's live updates).

Pass `waitForFinish` (seconds, up to 60) to wait for the run to reach a terminal status. The response comes as soon as the run finishes, or after 45 seconds with the run in its current state (see [Waiting for runs and builds](https://scrape.ly/docs/api-overview.md#waiting-for-runs-and-builds)). To wait longer, repeat the request until `status` is terminal. Without `waitForFinish`, the endpoint returns immediately.

```bash [Request]
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt
```

```json [Response]
{
  "data": {
    "id": "a7XyZ8kLmN2pQ4rSt",
    "actId": "eLN5rTB8sYTBWdfzx",
    "buildId": "bUiLdIdExAmPlE123",
    "status": "SUCCEEDED",
    "startedAt": "2026-08-14T15:00:00.000Z",
    "finishedAt": "2026-08-14T15:02:31.000Z",
    "options": {
      "build": "latest",
      "timeoutSecs": 3600,
      "memoryMbytes": 2048,
      "diskMbytes": 4096,
      "restartOnError": false
    },
    "usageUsd": { "ACTOR_COMPUTE_UNITS": 0.0125, "DATASET_WRITES": 0.0002 },
    "usageTotalUsd": 0.0127,
    "usageTotalNd": 12700000,
    "defaultDatasetId": "dS9t8uVwXyZ12345",
    "defaultKeyValueStoreId": "kVxYzAbCdEf12345",
    "defaultRequestQueueId": "rQmNoPqRsT123456"
  }
}
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs/{runId}" }
```

`usageUsd` breaks the run's cost down by service and `usageTotalUsd` is the total, both in US dollars. `usageTotalNd` is the same total in nanodollars (`$1 = 1_000_000_000`), an exact integer for accounting. Run lists and [search](https://scrape.ly/docs/api-misc.md) return `usageTotalUsd` and `usageTotalNd` too.

Read tabular results from `defaultDatasetId` ([dataset items](https://scrape.ly/docs/api-datasets.md)). Named output such as `OUTPUT` is a record in `defaultKeyValueStoreId`. You can also reach both through the [run storage shortcuts](#run-storages). There is no `/output` run endpoint.

## Get run input

```
GET /v2/actor-runs/{runId}/input
```

Returns the `INPUT` record from the run's default key-value store as **raw JSON**, not wrapped in `data`. If the run has no input, the body is `null`.

[Secret input fields](https://scrape.ly/docs/security.md#secret-input-fields) come back encrypted (`ENCRYPTED_VALUE:...`), including for your API key. Only the run's own token gets them decrypted, so `Actor.getInput()` inside the run sees plain values.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/input
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs/{runId}/input" }
```

## Run storages

```
GET /v2/actor-runs/{runId}/dataset
GET /v2/actor-runs/{runId}/dataset/items
GET /v2/actor-runs/{runId}/key-value-store
GET /v2/actor-runs/{runId}/key-value-store/keys
GET /v2/actor-runs/{runId}/key-value-store/records/{recordKey}
GET /v2/actor-runs/{runId}/request-queue
GET /v2/actor-runs/{runId}/request-queue/head
GET /v2/actor-runs/{runId}/request-queue/requests
```

Any path under `/v2/actor-runs/{runId}/dataset`, `/key-value-store`, or `/request-queue` is forwarded to the run's **default** storage. It behaves exactly like the matching [dataset](https://scrape.ly/docs/api-datasets.md), [key-value store](https://scrape.ly/docs/api-key-value-stores.md), or [request queue](https://scrape.ly/docs/api-request-queues.md) endpoint, with every method and parameter, so you don't need to look up the storage ID first.

Only the run owner, or the run's own scoped token, can use these paths. An unknown run returns `404 run-not-found`. A run without that default storage returns `404 record-not-found`.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/dataset/items?format=csv&limit=100"
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs/{runId}/dataset/items?limit=10" }
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs/{runId}/key-value-store/records/OUTPUT" }
```

## Update run

```
PUT /v2/actor-runs/{runId}
```

| Body field | Type | Description |
| --- | --- | --- |
| `statusMessage` | string | Message shown on the run. |
| `isStatusMessageTerminal` | boolean | Whether the status message is final. |
| `generalAccess` | string | `FOLLOW_USER_SETTING`, `ANYONE_WITH_ID_CAN_READ`, or `RESTRICTED`. |
| `id` | string | If present, must match `{runId}`. |

An `id` that doesn't match, or an unknown `generalAccess` value, returns `400 invalid-request`. Returns `200` with the updated run under `data`.

```bash
curl -X PUT https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"statusMessage": "Crawling page 12/40"}'
```

## Delete run

```
DELETE /v2/actor-runs/{runId}
```

Deletes a run that is `READY` or has finished. A run that is still `RUNNING`, `ABORTING`, or `TIMING-OUT` returns `400 invalid-state`, so abort it first. Returns `204 No Content`.

Deleting a run removes the run record. Its default storages are not deleted right away; they expire with your storage retention period.

```bash
curl -X DELETE https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt \
  -H "Authorization: Bearer $SCRAPELY_TOKEN"
```

## Abort run

```
POST /v2/actor-runs/{runId}/abort
```

Stops a run. The run moves to `ABORTING` and then `ABORTED`. Pass `gracefully=1` (or `true`) to give the Actor time to shut down cleanly instead of stopping the container at once. A `READY` run that hasn't started yet goes straight to `ABORTED`. Aborting a run that already finished, or is already `ABORTING`, changes nothing and returns the run as it is.

Returns `200 OK` with the run under `data`:

```json
{
  "data": {
    "id": "a7XyZ8kLmN2pQ4rSt",
    "status": "ABORTING"
  }
}
```

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/abort?gracefully=1"
```

## Metamorph run

```
POST /v2/actor-runs/{runId}/metamorph
```

Turns a **RUNNING** run into a run of a different Actor, keeping the same run ID and storages. A run can metamorph at most **10** times.

| Parameter | Where | Description |
| --- | --- | --- |
| `targetActorId` | body or query | Required. Target Actor ID or `username~name`. `actorId` is accepted as an alias. |
| `input` | body | New input for the target Actor (not `runInput`). |
| `build` | body or query | Target build tag or number. Only for a target Actor you own; on someone else's Actor it's ignored and the [User Build](https://scrape.ly/docs/publishing.md#user-build) is used. |

```bash
curl -X POST https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/metamorph \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "targetActorId": "otheruser~other-actor",
    "input": { "url": "https://example.com" },
    "build": "latest"
  }'
```

The previous `INPUT` is stored as `INPUT-METAMORPH-N` in the default key-value store.

Returns `200 OK` with the run under `data`, including a `metamorphs` array. Each item has `createdAt`, `actorId`, `buildId` (only for Actors you own), and `inputKey`. If the target Actor has no matching build, the run's `buildId` and `buildNumber` are cleared after the switch. The new container gets the target build's `ACTOR_BUILD_ID`, `ACTOR_BUILD_NUMBER`, and `ACTOR_BUILD_TAGS`, not the original Actor's. The container switches to the target Actor after the response, so `actId` in this response still shows the original Actor. Read the run again to see the new `actId`.

```json
{
  "data": {
    "id": "a7XyZ8kLmN2pQ4rSt",
    "actId": "eLN5rTB8sYTBWdfzx",
    "status": "RUNNING",
    "metamorphs": [
      {
        "createdAt": "2026-08-14T15:00:00.000Z",
        "actorId": "targetActorId123",
        "buildId": "bUiLdIdExAmPlE123",
        "inputKey": "INPUT-METAMORPH-1"
      }
    ]
  }
}
```

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-state` | The run is not `RUNNING`. |
| 400 | `limit-exceeded` | The run already metamorphed 10 times. |
| 403 | `insufficient-permissions` | A limited-permission run tried to metamorph into a full-permission Actor. |
| 403 | `full-permission-actor-not-approved` | The target Actor needs full permissions and you haven't approved it. See [Permission approvals](https://scrape.ly/docs/api-actors.md#permission-approvals). |
| 404 | `actor-not-found` | The target Actor does not exist. |

The wrong-state status code differs by endpoint: metamorph and resurrect return `400`, while [charge](#charge-events) returns `405`.

## Reboot run

```
POST /v2/actor-runs/{runId}/reboot
```

Restarts the run's container and keeps its storages. Allowed while the run is `READY` or `RUNNING`; any other status returns `400 invalid-run-status`. Returns `202`:

```json
{ "data": { "accepted": true, "runId": "a7XyZ8kLmN2pQ4rSt" } }
```

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/reboot
```

## Resurrect run

```
POST /v2/actor-runs/{runId}/resurrect
```

Restarts a finished run **in place**, with the same run ID and storages. The run must be `SUCCEEDED`, `FAILED`, `ABORTED`, or `TIMED-OUT`. Returns `200` with the run under `data` and a `Location` header. The run goes back to `RUNNING`, and an `ACTOR.RUN.RESURRECTED` webhook event fires.

Optional query parameters (defaults come from the original run): `build`, `timeout`, `memory`, `maxItems`, `maxTotalChargeUsd`, `restartOnError`. `build` is owner only; on someone else's Actor the run keeps its original build, or runs the [User Build](https://scrape.ly/docs/publishing.md#user-build) if that build was deleted.

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-state` | The run has not finished. |
| 400 | `invalid-memory` / `limit-exceeded` | The memory is invalid or would exceed your total memory limit. |
| 402 | `payment-required` | Not enough credit to start the run. |
| 429 | `concurrency-limit-exceeded` / `run-capacity-check-busy` | You are at your concurrent run limit, or another run request is being evaluated. |

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/resurrect?memory=4096"
```

## Charge events

```
POST /v2/actor-runs/{runId}/charge
```

Charges pay-per-event events against a **RUNNING** run you own. The SDK charging helpers and `scrapely actor charge` call this endpoint.

| Body field | Type | Description |
| --- | --- | --- |
| `eventName` | string | Required. An event name from the Actor's pricing. 1–128 bytes, starts with a letter or digit, then letters, digits, `.`, `_`, `:`, or `-`. |
| `count` | integer | Number of events, from `1` to `1000000`. Default `1`. |

The dataset-item event (`scrapely-default-dataset-item`) is charged automatically when items are pushed and cannot be charged through this endpoint.

Send an `Idempotency-Key` header (up to 128 characters of letters, digits, `.`, `_`, `:`, or `-`) whenever the charge is in a code path that can retry. Without it, a retry after a network error charges twice. A replay with the same key and payload returns the original result with `duplicate: true`. Keys are remembered for at least 35 days after the run ends, and a replay works even after the run has finished. If the charge was [voided](#void-a-charge), the replay returns `acceptedCount: 0`.

Response (`201`, not wrapped in `data`). `operationId` is your `Idempotency-Key`, or a generated ID if you didn't send one:

```json
{
  "operationId": "charge-1",
  "eventName": "result-item",
  "acceptedCount": 10,
  "requestedCount": 10,
  "duplicate": false
}
```

If the charge would go over the run's `maxTotalChargeUsd`, only part of it may be accepted: compare `acceptedCount` with `requestedCount`.

| Status | Type | Meaning |
| --- | --- | --- |
| 201 | — | Charge recorded, or replayed for a repeated `Idempotency-Key`. |
| 400 | `invalid-input` | Bad `eventName`, `count`, or `Idempotency-Key`. `error.code` says which: `event-name-invalid`, `event-name-too-long`, `event-name-server-derived`, `count-invalid`, `count-too-large`, `idempotency-key-invalid`, or `idempotency-key-too-long`. |
| 400 | `unknown-event` | The Actor's pricing has no event with that name. |
| 402 | `charge-limit-exceeded` | The run already reached `maxTotalChargeUsd`. Next to `error`, the body includes `acceptedCount: 0` and `requestedCount`. |
| 403 | `forbidden` | Not your run. |
| 404 | `run-not-found` | The run does not exist. |
| 405 | `invalid-state` | The run is not `RUNNING` (and the request is not a replay). |
| 409 | `conflict` | The `Idempotency-Key` was already used with a different event or count. |
| 409 | `ledger-sealed` | The run has finished and its charges were finalized. |
| 503 | `ledger-unavailable` | Charging is temporarily unavailable. Retry with the same `Idempotency-Key`. |

```bash
curl -X POST https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/charge \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: charge-1" \
  -d '{"eventName": "result-item", "count": 10}'
```

## Void a charge

```
POST /v2/actor-runs/{runId}/charge/void
```

Reverses a charge whose items were never stored, so a result is only billed if it was saved. The SDKs call this when a push fails after its charge was accepted. No body. The `Idempotency-Key` header is required and is the key of the charge to void.

Only the run's own token can void its charges; a user API token gets `403 forbidden`. Voiding works until the run is settled.

Response (`200`, not wrapped in `data`). `status` is `voided`, or `already-voided` for a repeated void. `voidedCount` is the number of events reversed:

```json
{ "status": "voided", "voidedCount": 10 }
```

| Status | Type | Meaning |
| --- | --- | --- |
| 200 | — | Charge voided, or already voided. |
| 400 | `invalid-input` | The `Idempotency-Key` is missing, invalid, or too long. `error.code` says which: `idempotency-key-required`, `idempotency-key-invalid`, or `idempotency-key-too-long`. |
| 403 | `forbidden` | Not your run, or not the run's own token. |
| 404 | `run-not-found` | The run does not exist. |
| 404 | `charge-not-found` | No charge with this `Idempotency-Key` was applied to the run. |
| 409 | `ledger-sealed` | The run has been settled; its charges can no longer be voided. |
| 503 | `ledger-unavailable` | The void could not be confirmed. Retry with the same `Idempotency-Key`. |

```bash
curl -X POST https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/charge/void \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Idempotency-Key: charge-1"
```

## Get run log

```
GET /v2/actor-runs/{runId}/log
```

Returns the log as **plain text** (`Content-Type: text/plain`), not a JSON envelope. There is no `/log/stream` endpoint.

On runs of an Actor you don't own, Scrape.ly's own log lines name no build: image names (which carry the build number) are replaced with `[actor image]`. The same applies to older runs whose log is read as the `log` record of the run's key-value store.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/a7XyZ8kLmN2pQ4rSt/log
```

```api-test
{ "method": "GET", "path": "/v2/actor-runs/{runId}/log" }
```

For the last run of an Actor or task, see [`GET /v2/actors/{actorId}/runs/last/log`](https://scrape.ly/docs/api-actors.md#last-run) and [`GET /v2/actor-tasks/{taskId}/runs/last/log`](https://scrape.ly/docs/api-tasks.md#last-task-run).

## Get log by ID

```
GET /v2/logs/{logId}
```

Returns the log of a run or a build by its ID, as **plain text**. The ID is checked as a run first, then as a build. Owner only: someone else's run returns `403 forbidden`, and an unknown build or one you can't read returns `404 record-not-found`.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/logs/a7XyZ8kLmN2pQ4rSt
```

---

- Previous: [Actors](https://scrape.ly/docs/api-actors.md)
- Next: [Actor tasks](https://scrape.ly/docs/api-tasks.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
