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.
| Part | What it is |
|---|---|
| Source code | JavaScript, TypeScript, or Python, using the JavaScript SDK (scrapely) or the Python SDK (scrapely) |
| Actor config | actor.json: name, version, schemas, memory limits, environment variables |
| Versions | Each with its own source and environment variables |
| Builds | Docker images made from a version |
| Runs | Executions of a build with one input |
| Settings | Defaults 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:
.actor/actor.json.actor/scrapely.jsonscrapely.jsonactor.json
If a project has both .actor/ config and a root scrapely.json, fields in the root file override the .actor/ ones.
{
"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
| Field | Type | Description |
|---|---|---|
actorSpecification | number | Always 1 |
name | string | Actor name. scrapely push creates the Actor with it |
title, description | string | Used when scrapely push creates the Actor |
version | string | MAJOR.MINOR version that scrapely push uploads to |
buildTag | string | Tag for successful builds of this version. Default latest |
input (alias inputSchema) | path or object | Input schema |
output (alias outputSchema) | path or object | Output schema |
storages | object | Storage schemas and extra storages |
environmentVariables | object | Variables for the version. A value starting with @ refers to a secret |
minMemoryMbytes, maxMemoryMbytes | number | Run memory is clamped to this range |
usesStandbyMode | boolean | true turns on Standby at build time if the Actor has no Standby config yet |
dockerfile | path | Dockerfile to build. Default .actor/Dockerfile, then Dockerfile |
dockerContextDir | path | Docker build context, relative to the config. Can point above the Actor folder in a Git monorepo |
readme, changelog | path | Default .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:
scrapely validate-schemaLifecycle
- The platform starts your container and injects the run environment: run token, run ID, default storage IDs, timeout, memory, proxy settings, and
CDP_URLfor the Scrape.ly browser. See JavaScript environment and Python environment. Actor.init(), whichActor.main()calls for you, connects storage, events, and charging.- Your code reads input, does the work, and writes to storage.
Actor.exit()flushes pending writes and exits. If the process crashes, the run isFAILEDand 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:
{
"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"]
}| Field | Dashboard control |
|---|---|
string | Text input. A password input when isSecret is true or format is password |
integer, number | Number input |
boolean | Toggle |
Any field with enum | Dropdown |
array | Repeating list of items |
object with properties | Nested 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": truemasks the value in saved input and encrypts it in the run's stored input. See Security."editor": "resourcePicker"with aresourceTypelets 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
propertieslists 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.
{
"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.
| Key | Where | Type | Effect |
|---|---|---|---|
categories | Schema root | object | Map of category key → settings. Categories appear in the order you declare them |
title | Category | string | Section heading. Defaults to the category key |
description | Category | string | Shown under the heading |
defaultCollapsed | Category | boolean | true starts the section collapsed. Default false. collapsed and default_contracted are accepted aliases |
parent | Category | string | Key of the category this one sits inside |
category | Property | string | Key of the category the field belongs to. Top-level properties only |
- Fields without a
categorycome 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
parentthat 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,
categoriesis 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.
sectionCaptionandsectionDescriptionalso 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 acategoryignoressectionCaption.- A
categorythat 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-schematreats 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:
| Storage | Use it for |
|---|---|
| Dataset | Result items: Actor.pushData() / Actor.push_data() |
| Key-value store | Files and single records. OUTPUT is the usual key for a single result |
| Request queue | The 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:
{
"storages": {
"dataset": "./dataset_schema.json",
"keyValueStores": {
"default": "./kvs_schema.json",
"screenshots": "./screenshots_schema.json"
}
}
}| Key | Effect |
|---|---|
dataset, keyValueStore, requestQueue | Schema for the run's default storage of that type |
datasets, keyValueStores, requestQueues | Map 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) orActor.open_key_value_store(alias="screenshots")(Python). The aliases reach the run inACTOR_STORAGES_JSON. - A dataset schema with
fields(JSON Schema draft-07) validates every pushed item. If one fails, nothing is stored and the push returns400 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:
{
"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 variable | Value |
|---|---|
links.apiDefaultDatasetUrl, links.apiDefaultKeyValueStoreUrl | API URL of the run's default dataset or key-value store |
links.apiRunUrl, links.consoleRunUrl, links.publicRunUrl | The run in the API and the dashboard |
storages.datasets.<alias>.apiUrl | API 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/...andgit@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.
/v2/actors/:actorId/versions/v2/actors/:actorId/versions/v2/actors/:actorId/versions/:versionNumber/v2/actors/:actorId/versions/:versionNumber/v2/actors/:actorId/versions/:versionNumber/v2/actors/:actorId/versions/:versionNumber/env-varsGET 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.jsonand 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:
| Image | Versions |
|---|---|
scrapely/actor-node, -playwright, -playwright-chrome, -playwright-firefox, -playwright-webkit, -playwright-camoufox, -puppeteer-chrome | Node.js 20, 22, 24, 26 |
scrapely/actor-python, -playwright, -playwright-chrome, -playwright-firefox, -playwright-webkit, -playwright-camoufox, -selenium | Python 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": {...}}:
| Option | Default | Notes |
|---|---|---|
memoryMbytes | 4096, or your plan's maximum if lower | A power of two |
timeoutSecs | Your plan's maximum | Can't exceed it |
noCache | false | Rebuild 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, orABORTED. - A successful build gets its version's build tag (
latestby default).latestalways 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 holdslateststays untagged. If the holder leavesSUCCEEDED,latestmoves 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
latestunless you passbuildor 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).
/v2/actors/:actorId/builds/v2/actors/:actorId/builds/v2/actors/:actorId/builds/default/v2/actor-builds/:buildId/v2/actor-builds/:buildId/abort/v2/actor-builds/:buildId/logBuilds, 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 card | Setting | API field | Default |
|---|---|---|---|
| Default run options | Default build | defaultRunOptions.build | latest |
| Memory | defaultRunOptions.memoryMbytes | 2048 MB | |
| Timeout | defaultRunOptions.timeoutSecs | 3600 s | |
| Maximum cost per run | defaultRunOptions.minimalMaxTotalChargeUsd | None | |
| Restart on error | defaultRunOptions.restartOnError | false | |
| (API only) | defaultRunOptions.maxItems, defaultRunOptions.forcePermissionLevel | None | |
| Continuous integration | GitHub webhook URL and workflow | CI webhooks | |
| Integrations | Example input body and content type | exampleRunInput | None |
| Standby Mode | Enable, memory, idle timeout, build, requests per instance | actorStandby | Off. See Standby |
| Actor permissions | Limited or full | actorPermissionLevel | LIMITED_PERMISSIONS. Locked once published. See Security |
| Scrapely Browser | Use Scrapely browser | PUT /v2/actors/:actorId/browser-patch | Off. 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 callfor a platform run,scrapely runfor 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:
| Option | Default | Notes |
|---|---|---|
timeout | 3600 seconds | A positive whole number of seconds, or 400 invalid-timeout |
memory | 2048 MB | A power of two (128, 256, 512, …) within your plan's limits and the Actor's minMemoryMbytes/maxMemoryMbytes |
build | latest | A build tag or number. Ignored on someone else's Actor, which always runs the User Build. |
maxItems | None | Passed 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 |
maxTotalChargeUsd | Actor's minimalMaxTotalChargeUsd | Spending cap for the run: usage, browser credits, LLM gateway requests, and pay-per-event charges. See Publish and monetize |
restartOnError | false | true or 1 restarts the run if it fails |
forcePermissionLevel | Actor's level | LIMITED_PERMISSIONS runs any Actor with limited permissions |
waitForFinish | 0 | Wait for the run to finish before responding. Accepts up to 60; the wait stops at 45 seconds |
webhooks | None | Base64-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.
| Response | Meaning |
|---|---|
201 | Run created |
202 | Queued for capacity, with meta.queuePosition and meta.estimatedWaitSecs |
429 concurrency-limit-exceeded | You're at your concurrent run limit |
400 limit-exceeded | The 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:
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:
| Setting | Default |
|---|---|
isEnabled | false |
desiredRequestsPerActorRun | 100 |
maxRequestsPerActorRun | None |
idleTimeoutSecs | 300 |
build | latest. Other users' standby runs use the User Build when it's set. |
memoryMbytes | 2048 |
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
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.