Skip to content
Build Actors

How Actors work

Lifecycle, input schema, versions, builds, runs, and Standby.

An Actor is a program that runs in a container on Scrape.ly. You write the code and an input schema; the platform builds the image, runs it on demand or on a schedule, stores the results, and bills the compute.

import { Actor } from 'scrapely';

await Actor.main(async () => {
    const input = await Actor.getInput();
    await Actor.pushData({ received: input });
});

Actor.main() initializes the SDK, runs your function, and exits with the right status. Don't also wrap the callback in async with Actor:.

What is an Actor?

One unit of automation: usually a scraper, but any job works, such as enriching records, monitoring prices, or processing files.

PartWhat it is
Source codeJavaScript, TypeScript, or Python, using the JavaScript SDK (scrapely) or the Python SDK (scrapely)
Actor configactor.json: name, version, schemas, memory limits, environment variables
VersionsEach with its own source and environment variables
BuildsDocker images made from a version
RunsExecutions of a build with one input
SettingsDefaults and switches you set in the dashboard or API: run defaults, Standby, permissions, the Scrape.ly browser

Project layout

The platform uses the first Actor config it finds:

  1. .actor/actor.json
  2. .actor/scrapely.json
  3. scrapely.json
  4. actor.json

If a project has both .actor/ config and a root scrapely.json, fields in the root file override the .actor/ ones.

.json
.actor/actor.json
{
    "actorSpecification": 1,
    "name": "my-actor",
    "title": "My Actor",
    "version": "0.0",
    "buildTag": "latest",
    "input": "./input_schema.json",
    "storages": { "dataset": "./dataset_schema.json" },
    "environmentVariables": { "API_KEY": "@myApiKey" },
    "minMemoryMbytes": 512,
    "maxMemoryMbytes": 4096
}

Config fields

FieldTypeDescription
actorSpecificationnumberAlways 1
namestringActor name. scrapely push creates the Actor with it
title, descriptionstringUsed when scrapely push creates the Actor
versionstringMAJOR.MINOR version that scrapely push uploads to
buildTagstringTag for successful builds of this version. Default latest
input (alias inputSchema)path or objectInput schema
output (alias outputSchema)path or objectOutput schema
storagesobjectStorage schemas and extra storages
environmentVariablesobjectVariables for the version. A value starting with @ refers to a secret
minMemoryMbytes, maxMemoryMbytesnumberRun memory is clamped to this range
usesStandbyModebooleantrue turns on Standby at build time if the Actor has no Standby config yet
dockerfilepathDockerfile to build. Default .actor/Dockerfile, then Dockerfile
dockerContextDirpathDocker build context, relative to the config. Can point above the Actor folder in a Git monorepo
readme, changelogpathDefault .actor/README.md, then README.md

Without input, the platform looks for .actor/input_schema.json, .actor/INPUT_SCHEMA.json, input_schema.json, INPUT_SCHEMA.json, then schema.json. Paths resolve relative to the config file.

Check every schema the config references before you push:

.bash
scrapely validate-schema

Lifecycle

  1. The platform starts your container and injects the run environment: run token, run ID, default storage IDs, timeout, memory, proxy settings, and CDP_URL for the Scrape.ly browser. See JavaScript environment and Python environment.
  2. Actor.init(), which Actor.main() calls for you, connects storage, events, and charging.
  3. Your code reads input, does the work, and writes to storage.
  4. Actor.exit() flushes pending writes and exits. If the process crashes, the run is FAILED and its log is kept.

The SDK doesn't start an HTTP server. If you need one, for Standby for example, listen on the port the platform gives you.

Input

Input is the JSON object your Actor gets at the start of a run. The input schema describes it and drives the dashboard form:

.json
input_schema.json
{
    "title": "Listing scraper input",
    "type": "object",
    "schemaVersion": 1,
    "properties": {
        "startUrl": {
            "title": "Start URL",
            "type": "string",
            "description": "Where the crawl begins",
            "editor": "textfield",
            "prefill": "https://example.com"
        },
        "maxItems": { "title": "Max items", "type": "integer", "default": 50, "minimum": 1 },
        "proxyConfiguration": {
            "title": "Proxy",
            "type": "object",
            "editor": "proxy",
            "default": { "useScrapelyProxy": true }
        }
    },
    "required": ["startUrl"]
}
FieldDashboard control
stringText input. A password input when isSecret is true or format is password
integer, numberNumber input
booleanToggle
Any field with enumDropdown
arrayRepeating list of items
object with propertiesNested group of fields
"editor": "proxy"Managed-proxy toggle or custom proxy URLs. See Proxy
"editor": "requestListSources"List of start URLs, saved as [{ "url": "..." }]
"editor": "hidden"Not shown

Other editor values are accepted and fall back to the control for the field's type. Users can always switch to a raw JSON editor.

  • "isSecret": true masks the value in saved input and encrypts it in the run's stored input. See Security.
  • "editor": "resourcePicker" with a resourceType lets a limited-permission Actor access the storage IDs a user passes in.
  • An input schema can be up to 500 KB.
  • The form shows fields in the order properties lists them, so put the important ones first.

Group fields into categories

An Actor with many options can keep the form short: leave the 1–3 main inputs ungrouped and put the rest into collapsible categories. Categories only change how the form looks. They don't change the input, defaults, validation or runs.

.json
input_schema.json
{
    "title": "Google Search Scraper",
    "type": "object",
    "schemaVersion": 1,
    "categories": {
        "filters": {
            "title": "Search filters",
            "description": "Narrow results by site, words and dates.",
            "defaultCollapsed": true
        },
        "dates": { "title": "Date range", "parent": "filters" },
        "ads-related": { "title": "Related to Ads", "defaultCollapsed": true }
    },
    "properties": {
        "queries": { "title": "Search queries", "type": "string", "editor": "textarea" },
        "site": { "title": "Site", "type": "string", "editor": "textfield", "category": "filters" },
        "afterDate": { "title": "After date", "type": "string", "editor": "textfield", "category": "dates" },
        "liveAiOverview": { "title": "Live AI overview", "type": "boolean", "category": "ads-related" }
    },
    "required": ["queries"]
}

Here queries is the main input, Search filters starts collapsed, and Date range sits inside it.

KeyWhereTypeEffect
categoriesSchema rootobjectMap of category key → settings. Categories appear in the order you declare them
titleCategorystringSection heading. Defaults to the category key
descriptionCategorystringShown under the heading
defaultCollapsedCategorybooleantrue starts the section collapsed. Default false. collapsed and default_contracted are accepted aliases
parentCategorystringKey of the category this one sits inside
categoryPropertystringKey of the category the field belongs to. Top-level properties only
  • Fields without a category come first, then each category with its fields in schema order, then its nested categories.
  • Categories nest up to 3 levels deep. A deeper category is shown at level 3. A parent that loops back (a → b → a) or names no category puts the category at the top level.
  • A category with no fields, directly or through nested categories, isn't shown.
  • A schema can define up to 100 categories. With more, categories is ignored and every field is shown ungrouped.
  • Each section header shows how many fields it holds and how many are set to a non-default value. A section opens on its own when one of its fields has an error or is required and empty.
  • sectionCaption and sectionDescription also work: a field with a caption starts a collapsed section that runs to the next caption. These sections come after your categories. A field with a category ignores sectionCaption.
  • A category that names no defined category doesn't fail the build. The build log warns, with the closest key (unknown category "ad-related". Did you mean "ads-related"?), and the field is shown ungrouped. scrapely validate-schema treats it as an error, along with parent loops, nesting deeper than 3 and wrong types, and warns about unused categories.

Read input with Actor.getInput() / Actor.get_input(), or getInputOrThrow() / get_input_or_throw() when missing input should fail the run.

Output

Results go to storage, not to files in the container:

StorageUse it for
DatasetResult items: Actor.pushData() / Actor.push_data()
Key-value storeFiles and single records. OUTPUT is the usual key for a single result
Request queueThe crawl frontier

Each run has its own default dataset, key-value store, and request queue. See Storage.

Storage schemas

storages in the config attaches schemas to a run's storages and can create extra ones:

.json
{
    "storages": {
        "dataset": "./dataset_schema.json",
        "keyValueStores": {
            "default": "./kvs_schema.json",
            "screenshots": "./screenshots_schema.json"
        }
    }
}
KeyEffect
dataset, keyValueStore, requestQueueSchema for the run's default storage of that type
datasets, keyValueStores, requestQueuesMap of alias → schema. default is the default storage. Every other alias gets its own storage for each run, as long as the map has a default entry
  • Open an extra storage by alias: Actor.openKeyValueStore({ alias: 'screenshots' }) (JavaScript) or Actor.open_key_value_store(alias="screenshots") (Python). The aliases reach the run in ACTOR_STORAGES_JSON.
  • A dataset schema with fields (JSON Schema draft-07) validates every pushed item. If one fails, nothing is stored and the push returns 400 schema-validation-error. See Datasets API.

Output schema

An output schema tells users where a run's results are. Each property's template becomes a link on the run's Output tab and in the run object's output map:

.json
output_schema.json
{
    "actorOutputSchemaVersion": 1,
    "title": "Output",
    "properties": {
        "results": {
            "type": "string",
            "title": "Results",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
        },
        "screenshots": {
            "type": "string",
            "title": "Screenshots",
            "template": "{{storages.keyValueStores.screenshots.apiUrl}}/keys"
        }
    }
}
Template variableValue
links.apiDefaultDatasetUrl, links.apiDefaultKeyValueStoreUrlAPI URL of the run's default dataset or key-value store
links.apiRunUrl, links.consoleRunUrl, links.publicRunUrlThe run in the API and the dashboard
storages.datasets.<alias>.apiUrlAPI URL of a dataset alias. Same for keyValueStores and requestQueues
run.<field>Any field of the run object, for example run.id

Unknown variables resolve to an empty string. The dashboard shows dataset links as a table and key-value store keys or records/<key> links as their contents.

Versions

An Actor has one or more versions, numbered MAJOR.MINOR (0.0, 0.1, 1.0). Each version has:

  • A source: uploaded files, a GitHub repository, or a tarball URL (up to 500 MB). Git sources accept https://github.com/... and git@github.com:... URLs only. A version's stored source can hold up to 10,000 files and 128 MB; see Versions for the upload limits.
  • Environment variables, plain or secret
  • A build tag (default latest) for its successful builds

scrapely push uploads the current directory as the source of the version in your config.

GET/v2/actors/:actorId/versions
POST/v2/actors/:actorId/versions
GET/v2/actors/:actorId/versions/:versionNumber
PUT/v2/actors/:actorId/versions/:versionNumber
DELETE/v2/actors/:actorId/versions/:versionNumber
GET/v2/actors/:actorId/versions/:versionNumber/env-vars

GET returns the version with its file contents, so you can read a version, change a field, and PUT it back without touching the source. See the Actors API.

Builds

A build turns one version's source into a Docker image. Only the owner can build. Builds start from:

  • scrapely push
  • The dashboard, on the Actor's Builds tab
  • The API: POST /v2/actors/:actorId/builds?version=0.0
  • The Actor's CI webhook, for Git versions. It pulls the latest commit first, and the build reads actor.json and the input schema from that commit.

Base images

Without a Dockerfile, the platform generates one: Node.js projects install package.json dependencies and start main.js, Python projects start main.py. With your own Dockerfile, start FROM a Scrape.ly base image, which already contains the SDK:

ImageVersions
scrapely/actor-node, -playwright, -playwright-chrome, -playwright-firefox, -playwright-webkit, -playwright-camoufox, -puppeteer-chromeNode.js 20, 22, 24, 26
scrapely/actor-python, -playwright, -playwright-chrome, -playwright-firefox, -playwright-webkit, -playwright-camoufox, -seleniumPython 3.11, 3.12, 3.13, 3.14

Tag the image with the version, for example scrapely/actor-node-playwright-chrome:22. Builds also accept the matching apify/actor-* images, but publishing to the Store requires a scrapely/actor-* base.

Options and limits

Pass options in the request body as {"options": {...}}:

OptionDefaultNotes
memoryMbytes4096, or your plan's maximum if lowerA power of two
timeoutSecsYour plan's maximumCan't exceed it
noCachefalseRebuild every layer

Build memory counts toward your account's total memory limit. Your plan's limits are under Billing → Limits.

Statuses and tags

  • A build goes READY → RUNNING → SUCCEEDED, FAILED, TIMED-OUT, or ABORTED.
  • A successful build gets its version's build tag (latest by default). latest always belongs to the newest succeeded build by start time (ties go to the higher build number), not to the build that finished last: a build that finishes after a newer one already holds latest stays untagged. If the holder leaves SUCCEEDED, latest moves back to the newest succeeded build. You can add your own tags: 1 to 64 letters, digits, ., _, and -, starting with a letter or digit.
  • Runs use latest unless you pass build or the Actor has a default build. Other users can't pick a build: they always run the User Build.
  • Each build records whether it uses the Scrape.ly browser (browserPatched).
GET/v2/actors/:actorId/builds
POST/v2/actors/:actorId/builds
GET/v2/actors/:actorId/builds/default
GET/v2/actor-builds/:buildId
POST/v2/actor-builds/:buildId/abort
GET/v2/actor-builds/:buildId/log

Builds, build logs, and versions are owner only, also for public Actors.

Actor settings

Per-Actor settings live on the Actor's Settings tab in the dashboard and on the Actor object in the API. Only the owner can change them.

Dashboard cardSettingAPI fieldDefault
Default run optionsDefault builddefaultRunOptions.buildlatest
MemorydefaultRunOptions.memoryMbytes2048 MB
TimeoutdefaultRunOptions.timeoutSecs3600 s
Maximum cost per rundefaultRunOptions.minimalMaxTotalChargeUsdNone
Restart on errordefaultRunOptions.restartOnErrorfalse
(API only)defaultRunOptions.maxItems, defaultRunOptions.forcePermissionLevelNone
Continuous integrationGitHub webhook URL and workflowCI webhooks
IntegrationsExample input body and content typeexampleRunInputNone
Standby ModeEnable, memory, idle timeout, build, requests per instanceactorStandbyOff. See Standby
Actor permissionsLimited or fullactorPermissionLevelLIMITED_PERMISSIONS. Locked once published. See Security
Scrapely BrowserUse Scrapely browserPUT /v2/actors/:actorId/browser-patchOff. Applies to the next build. See Scrape.ly Browser

Store listing fields (title, description, SEO, categories, icon, pricing, source visibility) and the User Build (the build other users run, defaultNonOwnerBuild) are on the Publication tab. See Publish and monetize.

Runs

A run executes one build with one input, and needs at least one successful build. Start runs from:

  • The dashboard
  • The CLI: scrapely call for a platform run, scrapely run for the local project on your machine
  • The API: POST /v2/actors/:actorId/runs
  • Tasks and schedules

Statuses: READY, RUNNING, SUCCEEDED, FAILED, ABORTING, ABORTED, TIMING-OUT, TIMED-OUT.

Run options

Query parameters on POST /v2/actors/:actorId/runs. Anything you leave out falls back to the Actor's defaultRunOptions, then the platform default:

OptionDefaultNotes
timeout3600 secondsA positive whole number of seconds, or 400 invalid-timeout
memory2048 MBA power of two (128, 256, 512, …) within your plan's limits and the Actor's minMemoryMbytes/maxMemoryMbytes
buildlatestA build tag or number. Ignored on someone else's Actor, which always runs the User Build.
maxItemsNonePassed to the run as ACTOR_MAX_PAID_DATASET_ITEMS. Result-item events past it aren't billed, so your code should stop at this count
maxTotalChargeUsdActor's minimalMaxTotalChargeUsdSpending cap for the run: usage, browser credits, LLM gateway requests, and pay-per-event charges. See Publish and monetize
restartOnErrorfalsetrue or 1 restarts the run if it fails
forcePermissionLevelActor's levelLIMITED_PERMISSIONS runs any Actor with limited permissions
waitForFinish0Wait for the run to finish before responding. Accepts up to 60; the wait stops at 45 seconds
webhooksNoneBase64-encoded JSON array of ad hoc webhooks for this run

Send an Idempotency-Key header to make a retried start return the same run instead of starting another. See Idempotent run starts.

The request body is the run's input.

ResponseMeaning
201Run created
202Queued for capacity, with meta.queuePosition and meta.estimatedWaitSecs
429 concurrency-limit-exceededYou're at your concurrent run limit
400 limit-exceededThe run would exceed your total memory

run-sync and run-sync-get-dataset-items start a run and wait for it, for at most 45 seconds. After that they return 408 with the run ID, and the run keeps going. Without timeout, they cap the run at 300 seconds. See Waiting for runs and builds.

Read a run's log with GET /v2/actor-runs/:runId/log or scrapely runs log <runId>. See the Runs API.

Standby

A Standby Actor keeps a container warm and serves HTTP requests instead of batch runs:

.text
ALL /v2/:username/:actorName/*

Callers send their own Bearer token and are billed for the run. The Actor must be public, or the caller must own it. Configure it under actorStandby or on the Standby Mode settings card:

SettingDefault
isEnabledfalse
desiredRequestsPerActorRun100
maxRequestsPerActorRunNone
idleTimeoutSecs300
buildlatest. Other users' standby runs use the User Build when it's set.
memoryMbytes2048

Listen on ACTOR_WEB_SERVER_PORT and bind to 0.0.0.0. Read the port with Actor.getEnv().containerPort (JavaScript) or Actor.get_env().container_port (Python). Standby runs have ACTOR_META_ORIGIN=STANDBY and ACTOR_STANDBY_URL. See Standby API and the JavaScript Standby guide.

Publishing

To list an Actor in the Store it needs a title, a description, a README, a category, a successful build, and an input schema. A Dockerfile must use a scrapely/actor-node (Node.js 20, 22, 24, or 26) or scrapely/actor-python (Python 3.11 or later) base. Most Actors go live immediately; some get a short manual review first. Set pay-per-event pricing and earn 80% of what users pay with purchased credits, paid out when you request it.

See Publish and monetize.

Full example

.js
import { Actor, CheerioCrawler } from 'scrapely';

await Actor.main(async () => {
    const { startUrl = 'https://news.ycombinator.com', maxItems = 25 } =
        (await Actor.getInput()) ?? {};

    const proxyConfiguration = await Actor.createProxyConfiguration();

    const crawler = new CheerioCrawler({
        proxyConfiguration,
        maxRequestsPerCrawl: maxItems,
        requestHandler: async ({ $, request }) => {
            await Actor.pushData({
                url: request.url,
                title: $('title').text().trim(),
            });
        },
    });

    await crawler.run([startUrl]);
});

Push it with scrapely push and run it with scrapely call. The results land in the run's default dataset, ready to export from the dashboard, the CLI, or the API.