Skip to content
JavaScript SDK

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:

OptionTypeMeaning
buildstringBuild tag or number to run. Ignored on someone else's Actor, which always runs the User Build.
memorynumberMemory in MB
timeoutnumber | 'inherit'Timeout in seconds, or 'inherit'
maxItemsnumberResult cap for Actors that charge per result item
maxTotalChargeUsdnumberSpending cap for pay-per-event Actors
restartOnErrorbooleanRestart the run once if it fails
webhooksarrayAd-hoc webhooks for the new run
tokenstringUse 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

.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:

OptionMeaning
gracefullySend persistState and aborting to the run and give it 30 seconds to stop
statusMessageSet as the run's terminal status message before aborting
tokenUse 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.
  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.

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.

.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}`,
});
FieldRequiredMeaning
eventTypesyesACTOR.RUN.CREATED, ACTOR.RUN.SUCCEEDED, ACTOR.RUN.FAILED, ACTOR.RUN.ABORTED, ACTOR.RUN.TIMED_OUT, ACTOR.RUN.RESURRECTED
requestUrlyesURL that receives the POST
payloadTemplatenoCustom payload template
headersTemplatenoCustom headers template
descriptionnoShown in the dashboard
idempotencyKeynoRepeated calls with the same key create only one webhook
ignoreSslErrorsnoSkip TLS verification of requestUrl
doNotRetrynoDon't retry failed deliveries
shouldInterpolateStringsnoInterpolate variables inside string values of the templates
isApifyIntegrationnoCompatibility 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.

.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:

FieldMeaning
chargedCountEvents actually charged
eventChargeLimitReachedtrue when the run can't afford another event of this type
chargeableWithinLimitFor 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');
}
SituationBehavior
Actor isn't priced per eventWarns once, charges nothing
Event isn't in the pricingWarns Attempting to charge for an unknown event '<name>', charges nothing
Platform rejects the chargeReturns chargedCount: 0 and eventChargeLimitReached: true
Platform doesn't confirm after retriesThrows ChargeUncertainError (with operationId, eventName, count)
Called after shutdown startedThrows 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:

MethodReturnsPurpose
getPricingInfo()ActorPricingInfopricingModel, isPayPerEvent, maxTotalChargeUsd, perEventPrices
getChargedEventCount(eventName)numberEvents of this type charged so far in this run
getMaxTotalChargeUsd()numberThe run's spending cap
calculateMaxEventChargeCountWithinLimit(eventName)numberHow 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).

Testing charging locally

VariableEffect
ACTOR_TEST_PAY_PER_EVENT=1Treats the Actor as pay-per-event locally. Every event costs 1
ACTOR_USE_CHARGING_LOG_DATASET=1Records every charge in a local dataset named charging_log (pay-per-event only)
ACTOR_MAX_TOTAL_CHARGE_USDBudget 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.