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
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
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
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. |
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 and the Actors API.
Abort a run
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
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.
- The SDK sends
POST /v2/actor-runs/{runId}/metamorphwithtargetActorId,input, andbuild. - The platform copies the current
INPUTrecord toINPUT-METAMORPH-{n}, writes the new input toINPUT, and restarts the container with the target Actor's image.buildonly applies when you own the target Actor; someone else's Actor runs its User Build. - 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.
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().
Webhooks
Actor.addWebhook() creates an ad-hoc webhook that only fires for the current run.
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.
Charging
Pay-per-event Actors charge users for events they define in their pricing. For setting up pricing, see Publishing.
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:
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:
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).
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.