The Actor programming model
The Actor programming model is a language-agnostic standard for shipping a web automation program as a serverless cloud service. An Actor is a Docker image that accepts one structured JSON input, runs to completion on managed infrastructure, writes its results to platform storage (a dataset for rows, a key-value store for files), and ends in a named run status that the API and Console read identically. Apify is the platform that implements it, and the model itself is published as an open specification in the Web Actor programming model whitepaper.
Four things make a program an Actor: JSON input, platform storage (datasets, key-value stores, request queues), optional proxy configuration, and a standardized run status. Everything inside the container is your own code, in any language that runs on Linux.
What is an Actor?
Apify's own definition: Actors are "serverless cloud programs that take a structured JSON input, perform a task (web scraping, browser automation, data processing, and more), and optionally produce a structured output" (docs.apify.com/platform/actors, checked 2026-09-10). In practice that means a program packaged as a Docker image that Apify schedules on shared infrastructure. Callers pass JSON input; the run writes structured rows to a dataset and optional files to a key-value store, then finishes in one of four terminal statuses the API and Console surface the same way.
Tools in the Apify Store, from Google Maps to Instagram, are Actors built on this model.
Design goals
- Reproducible: Same input and platform settings should yield comparable outputs, which helps testing and audits.
- Scalable: Many runs can execute in parallel; the platform handles placement and limits per plan.
- Composable: One Actor can enqueue or call others, or hand off via webhooks and orchestrators (n8n, Make, etc.).
- Portable: Anything that runs in Linux Docker can ship; Node.js and Python are the most common SDK paths.
Actor lifecycle
Every run follows the same coarse phases:
- Bootstrap: the run is created as
READY, meaning "started but not allocated to any worker yet". It becomesRUNNINGonce Apify places it on a worker and injects the environment (tokens, default storage IDs, proxy credentials if configured). - Input: Your code loads input JSON (from the API, Console, or task).
- Work: Crawling, browser automation, API calls, transforms.
- Output: Rows go to the default dataset (or another named store); binaries and snapshots often go to KV.
- Completion: the run settles in one of four terminal statuses:
SUCCEEDED,FAILED,TIMED-OUT, orABORTED.TIMING-OUTandABORTINGare the transitional states you will see in between (docs.apify.com, checked 2026-09-10). Logs and metrics stay queryable afterwards.
Poll for a terminal status rather than for SUCCEEDED alone, or an aborted run will hang your integration. For interface details (events, webhooks, run schema), see Actor interface.
Input and output schema
Runtime input (JSON)
At run time you read a single JSON object. JavaScript:
import { Actor } from 'apify';
await Actor.init();
const input = (await Actor.getInput()) ?? {};
const startUrl = input.startUrl ?? 'https://example.com';
const maxPages = input.maxPages ?? 10;
Python:
import asyncio
from apify import Actor
async def main():
async with Actor:
actor_input = await Actor.get_input() or {}
start_url = actor_input.get('startUrl', 'https://example.com')
if __name__ == '__main__':
asyncio.run(main())
Declared input (INPUT_SCHEMA.json)
Publishing Actors typically includes .actor/INPUT_SCHEMA.json so the Console can render a form, validate types, and document defaults. That file is the contract for integrators; the runtime object matches those fields.
Minimal example
{
"title": "Example scraper input",
"type": "object",
"schemaVersion": 1,
"properties": {
"startUrl": { "type": "string", "description": "First page to crawl" },
"maxPages": { "type": "integer", "default": 10 }
},
"required": ["startUrl"]
}
Output: datasets and key-value store
Actor.pushData(record)appends one JSON-serializable object (or an array of them) to the run's default dataset. The Console and API expose it as JSON, CSV, Excel, and other tabular exports.Actor.setValue(key, body, { contentType })writes a blob (HTML dumps, screenshots, PDFs, checkpoint JSON) to the run's key-value store. Default content type isapplication/json; setcontentTypefor binary payloads.
Consumers pull results through the Apify API (GET /v2/datasets/{id}/items, GET /v2/key-value-stores/{id}/records/{key}) or linked integrations.
Storage types (mental model)
| Storage | Best for | Typical access |
|---|---|---|
| Dataset | Many homogeneous records (products, posts, map pins) | pushData, API dataset items |
| Key-value store | Files, single JSON payloads, crawl state | setValue / getValue, named keys |
| Request queue | Crawler frontiers (URLs + metadata) | Crawlee / SDK queue APIs |
Default storages are scoped to the run; named storages let you share data across runs when you design for it.
Proxy configuration
Actors set proxies in code so each run picks the right group (datacenter vs residential) and country. JavaScript (inside an Actor):
import { Actor } from 'apify';
import { CheerioCrawler } from 'crawlee';
await Actor.init();
const proxyConfiguration = await Actor.createProxyConfiguration({
groups: ['RESIDENTIAL'],
countryCode: 'US',
});
const crawler = new CheerioCrawler({ proxyConfiguration });
// Pass proxyConfiguration to any Crawlee crawler; it rotates per session.
// For ad-hoc requests, call await proxyConfiguration.newUrl(sessionId) and
// feed the string to fetch / got-scraping / Playwright launch options.
await Actor.exit();
Python uses the same pattern: await Actor.create_proxy_configuration(groups=['RESIDENTIAL'], country_code='US'), then hand the result to a Crawlee crawler or call await proxy_configuration.new_url() for individual requests.
Exact group names and plan limits depend on your Apify account; configure rotation and session stickiness to match the target site.
Philosophy (why this shape?)
- Reproducible: Versioned Docker images + logged inputs make “what ran?” answerable.
- Operational: Logging, retries, and scheduling are platform features, not one-off shell scripts.
- Shareable: Store publishing reuses the same interface for strangers running your Actor.
- Integrable: Everything is API-first: start runs, poll status, stream dataset rows.
What the model costs to run
The model's unit of billing is the compute unit. One CU is 1 GB of memory running for one hour, so a run costs memory × duration × your plan's CU rate. Memory also buys CPU: every 4,096 MB of allocated memory gets one full CPU core, allocated proportionally below that (docs.apify.com, checked 2026-09-10).
A CU costs $0.20/CU subject to change · verified 2026-09-10 on the Free and Starter plans, $0.16 on Scale and $0.13 on Business. The Free plan includes $5 of platform usage a month; Starter is $19/month subject to change · verified 2026-09-10 and includes $19 of usage (apify.com/pricing, checked 2026-09-10). So a PlaywrightCrawler pinned at 4 GB burns 4 CU an hour, which is $0.80 an hour on Starter and eats the included usage in roughly 24 hours of run time.
Actors published on the Store can carry a second charge on top of that, because the developer sets the price: pay-per-event, where "users pay for specific events that are programmatically triggered from the Actor's source code", or pay-per-usage, where you pay only the platform usage the run generates (docs.apify.com, checked 2026-09-10). See Apify pricing for both layers and compute units for the counting rules.
Actors vs. traditional scripts
| Aspect | Traditional script | Actor |
|---|---|---|
| Hosting | Your server / laptop | Apify managed containers |
| Scaling | You provision | Parallel runs per limits |
| State | Custom DB / files | KV + datasets + queues |
| Proxies | DIY | First-class configuration |
| Scheduling | Cron on a VM | Built-in tasks & cron |
| Sharing | Git only | Store + input schema |
Getting started
- Scaffold with the CLI. See Build an Apify Actor.
- Define INPUT_SCHEMA.json early so API and Console users stay aligned.
- Push data incrementally with
pushDatainstead of holding huge arrays in memory.
It is Apify’s contract for serverless automation: Docker-packaged code that receives JSON input, uses platform-managed storage (datasets, key-value stores, queues), can use Apify proxies, and reports run status through a unified API and Console.
At runtime, code reads one JSON object via the SDK (for example Actor.getInput). For users and integrations, you declare fields and types in .actor/INPUT_SCHEMA.json so the Console renders a validated form and documentation stays accurate.
A dataset is an ordered list of JSON records, ideal for tabular export and analytics pipelines. A key-value store holds named blobs (screenshots, raw HTML, checkpoint JSON) addressed by key.
You create a proxy configuration from the Actor context, obtain rotating URLs, and pass them to your HTTP client or browser. Groups and country codes map to your Apify proxy product; combine with session options when a site requires sticky IPs.
Platform usage is billed in compute units, where 1 CU is 1 GB of memory running for one hour. A CU costs $0.20 on the Free and Starter plans, $0.16 on Scale and $0.13 on Business. The Free plan includes $5 of usage a month and Starter is $19 a month with $19 of usage included (apify.com/pricing, checked 2026-09-10). Actors published on the Apify Store can add a developer-set charge on top, usually pay-per-event.
Four terminal statuses: SUCCEEDED, FAILED, TIMED-OUT and ABORTED. READY and RUNNING come first, and TIMING-OUT and ABORTING are transitional. Integrations should poll for any terminal status rather than for SUCCEEDED alone.
Any language that fits in a Linux Docker image. JavaScript/TypeScript and Python have the richest Apify SDK support and examples; other stacks call the HTTP API or use thin SDK wrappers.