---
title: "Actors & webhooks · Scrape.ly Docs"
description: "Call, start, abort, and metamorph Actors, add webhooks, and charge per event."
url: https://scrape.ly/docs/sdk-node-actors
markdown: https://scrape.ly/docs/sdk-node-actors.md
---

# Actors & webhooks

> Call, start, abort, and metamorph Actors, add webhooks, and charge per event.

An Actor can start other Actors and tasks, abort runs, hand its run over to another Actor, attach webhooks to itself, and charge pay-per-event events. These methods go through the platform API with the run's token.

## Calling Actors and tasks

### Start without waiting

```ts
const run = await Actor.start('owner~child-actor', { url });
console.log(run.id, run.defaultDatasetId);
```

`start()` returns the new run as soon as it's created.

### Call and wait

```ts
const run = await Actor.call('owner~child-actor', { url }, { timeout: 300 });
const { items } = await Actor.newClient().dataset(run.defaultDatasetId).listItems();
```

`call()` starts the run, streams its log into your run's log, and waits until it finishes. Without `waitSecs` it waits indefinitely.

### Call a task

```ts
const run = await Actor.callTask('task-id', { url }, { timeout: 'inherit' });
```

`callTask()` merges the input you pass with the task's saved input.

### Options

`actorId` and `taskId` accept an ID or `username~name`. All three methods accept these options:

| Option | Type | Meaning |
| --- | --- | --- |
| `build` | `string` | Build tag or number to run. Ignored on someone else's Actor, which always runs the [User Build](https://scrape.ly/docs/publishing.md#user-build). |
| `memory` | `number` | Memory in MB |
| `timeout` | `number \| 'inherit'` | Timeout in seconds, or `'inherit'` |
| `maxItems` | `number` | Result cap for Actors that charge per result item |
| `maxTotalChargeUsd` | `number` | Spending cap for pay-per-event Actors |
| `restartOnError` | `boolean` | Restart the run once if it fails |
| `webhooks` | `array` | Ad-hoc webhooks for the new run |
| `token` | `string` | Use another token for this call |

`call()` and `callTask()` also accept `waitSecs` (how long to wait) and `log` (`null` turns off log streaming). `call()` and `start()` accept `contentType` for non-JSON input.

`timeout: 'inherit'` sets the child's timeout to your run's remaining time, rounded up to whole seconds, so the child never outlives the parent. If your run has no timeout (`ACTOR_TIMEOUT_AT` unset, e.g. locally), the SDK logs a warning and sends no timeout.

For the full run options, see [Run Actors](https://scrape.ly/docs/run-actors.md) and the [Actors API](https://scrape.ly/docs/api-actors.md).

### Abort a run

```ts
await Actor.abort();
await Actor.abort('run-id', { gracefully: true, statusMessage: 'Superseded by a newer run' });
```

With no run ID, `abort()` aborts the current run. It returns the updated run. Options:

| Option | Meaning |
| --- | --- |
| `gracefully` | Send `persistState` and `aborting` to the run and give it 30 seconds to stop |
| `statusMessage` | Set as the run's terminal status message before aborting |
| `token` | Use another token |

Without `gracefully`, the container gets 5 seconds after `SIGTERM` before it's killed.

### Metamorph

```ts
await Actor.metamorph('owner~next-stage', { url }, { build: 'latest' });
```

`metamorph(targetActorId, input?, options?)` replaces the running Actor with another one inside the same run. The run keeps its ID and default storages, so both stages' output lands in one place.

1. The SDK sends `POST /v2/actor-runs/{runId}/metamorph` with `targetActorId`, `input`, and `build`.
2. The platform copies the current `INPUT` record to `INPUT-METAMORPH-{n}`, writes the new input to `INPUT`, and restarts the container with the target Actor's image. `build` only applies when you own the target Actor; someone else's Actor runs its [User Build](https://scrape.ly/docs/publishing.md#user-build).
3. The SDK flushes storage and charges, then calls `process.exit(0)`. Code after the call doesn't run.

A run can metamorph at most 10 times. `metamorph()` throws without run credentials (locally) and when the API rejects the request. `MetamorphOptions.contentType` exists for compatibility but isn't sent.

> [!NOTE]
> In the target Actor, `Actor.getInput()` reads `INPUT-METAMORPH-{n}` first, and `ACTOR_INPUT` still holds the original run input. Both return the previous stage's input, not the one passed to `metamorph()`. Until this is fixed, read the new input with `await Actor.getValue('INPUT')`.

Use metamorph for stages that should look like one run. Use `call()` when you want a separate child run with its own ID and storages.

### Reboot

`Actor.reboot()` restarts the current container while keeping the run. See [`Actor.reboot()`](https://scrape.ly/docs/sdk-node-proxy-events.md#actorreboot).

## Webhooks

`Actor.addWebhook()` creates an ad-hoc webhook that only fires for the current run.

```ts
await Actor.addWebhook({
    eventTypes: ['ACTOR.RUN.SUCCEEDED', 'ACTOR.RUN.FAILED'],
    requestUrl: 'https://example.com/scrapely-hook',
    description: 'Notify the pipeline',
    idempotencyKey: `pipeline-${Actor.getEnv().actorRunId}`,
});
```

| Field | Required | Meaning |
| --- | --- | --- |
| `eventTypes` | yes | `ACTOR.RUN.CREATED`, `ACTOR.RUN.SUCCEEDED`, `ACTOR.RUN.FAILED`, `ACTOR.RUN.ABORTED`, `ACTOR.RUN.TIMED_OUT`, `ACTOR.RUN.RESURRECTED` |
| `requestUrl` | yes | URL that receives the `POST` |
| `payloadTemplate` | no | Custom payload template |
| `headersTemplate` | no | Custom headers template |
| `description` | no | Shown in the dashboard |
| `idempotencyKey` | no | Repeated calls with the same key create only one webhook |
| `ignoreSslErrors` | no | Skip TLS verification of `requestUrl` |
| `doNotRetry` | no | Don't retry failed deliveries |
| `shouldInterpolateStrings` | no | Interpolate variables inside string values of the templates |
| `isApifyIntegration` | no | Compatibility flag passed through to the API |

Outside the platform, `addWebhook()` logs `Actor.addWebhook() is only supported on the platform. Ignoring.` and returns `undefined`. For webhooks that fire for every run, see [Webhooks API](https://scrape.ly/docs/api-webhooks.md).

## Charging

Pay-per-event Actors charge users for events they define in their pricing. For setting up pricing, see [Publishing](https://scrape.ly/docs/publishing.md).

```ts
const result = await Actor.charge({ eventName: 'page-scraped', count: 1 });
console.log(result.chargedCount, result.eventChargeLimitReached);

await Actor.charge('page-scraped');      // count defaults to 1
await Actor.charge('page-scraped', 5);
```

`charge()` requires `Actor.init()`. `count` must be a positive integer. It returns a `ChargeResult`:

| Field | Meaning |
| --- | --- |
| `chargedCount` | Events actually charged |
| `eventChargeLimitReached` | `true` when the run can't afford another event of this type |
| `chargeableWithinLimit` | For each priced event, how many more fit in the budget |

Charges stop at the run's `maxTotalChargeUsd`. When `count` doesn't fit, `charge()` charges what fits plus one event that crosses the cap, and nothing after that. (`pushData()` with an event name never crosses the cap.) Check `eventChargeLimitReached` and stop working instead of continuing for free:

```ts
const result = await Actor.charge({ eventName: 'page-scraped' });
if (result.eventChargeLimitReached) {
    await Actor.exit('Charge limit reached');
}
```

| Situation | Behavior |
| --- | --- |
| Actor isn't priced per event | Warns once, charges nothing |
| Event isn't in the pricing | Warns `Attempting to charge for an unknown event '<name>'`, charges nothing |
| Platform rejects the charge | Returns `chargedCount: 0` and `eventChargeLimitReached: true` |
| Platform doesn't confirm after retries | Throws `ChargeUncertainError` (with `operationId`, `eventName`, `count`) |
| Called after shutdown started | Throws `ChargingManager is closed (actor is shutting down); charge rejected` |

The SDK flushes pending charges on exit, on `SIGTERM`/`SIGINT`, before `reboot()` and `metamorph()`, and on fatal errors. `ChargeRejectedError` is exported for type checks; `charge()` handles it internally.

### Charging manager

`Actor.getChargingManager()` returns the run's `ChargingManager`:

| Method | Returns | Purpose |
| --- | --- | --- |
| `getPricingInfo()` | `ActorPricingInfo` | `pricingModel`, `isPayPerEvent`, `maxTotalChargeUsd`, `perEventPrices` |
| `getChargedEventCount(eventName)` | `number` | Events of this type charged so far in this run |
| `getMaxTotalChargeUsd()` | `number` | The run's spending cap |
| `calculateMaxEventChargeCountWithinLimit(eventName)` | `number` | How many more of this event fit in the budget (`Infinity` for unpriced events) |
| `calculatePushDataLimits({ eventName, isDefaultDataset, items })` | `{ eventsToCharge, limitedItems }` | Trims a batch to what the budget allows |

Size a batch to the remaining budget before doing the work:

```ts
const manager = Actor.getChargingManager();
const room = manager.calculateMaxEventChargeCountWithinLimit('page-scraped');
const batch = pending.slice(0, room);
```

### Default dataset item event

If the pricing includes `apify-default-dataset-item` or `scrapely-default-dataset-item`, every item written to the default dataset is charged automatically, whichever way you write it. `DEFAULT_DATASET_ITEM_EVENT` is `apify-default-dataset-item`; `SCRAPELY_DEFAULT_DATASET_ITEM_EVENT` is the Scrape.ly name. The SDK uses whichever one your pricing declares, and `getChargedEventCount()` with either name returns the combined total.

Don't pass these names to `pushData()` or `charge()` yourself; `pushData()` throws if you do. To combine the per-item charge with your own event, pass your event to [`pushData(items, eventName)`](https://scrape.ly/docs/sdk-node-input-output.md#actorpushdata).

### Testing charging locally

| Variable | Effect |
| --- | --- |
| `ACTOR_TEST_PAY_PER_EVENT=1` | Treats the Actor as pay-per-event locally. Every event costs `1` |
| `ACTOR_USE_CHARGING_LOG_DATASET=1` | Records every charge in a local dataset named `charging_log` (pay-per-event only) |
| `ACTOR_MAX_TOTAL_CHARGE_USD` | Budget to test the limit against |

`ACTOR_TEST_PAY_PER_EVENT` and `ACTOR_USE_CHARGING_LOG_DATASET` are local-only; `Actor.init()` throws if either is set on a platform run.

---

- Previous: [Proxy & events](https://scrape.ly/docs/sdk-node-proxy-events.md)
- Next: [Environment](https://scrape.ly/docs/sdk-node-environment.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
