Skip to main content

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.

Quick Answer

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:

  1. Bootstrap: the run is created as READY, meaning "started but not allocated to any worker yet". It becomes RUNNING once Apify places it on a worker and injects the environment (tokens, default storage IDs, proxy credentials if configured).
  2. Input: Your code loads input JSON (from the API, Console, or task).
  3. Work: Crawling, browser automation, API calls, transforms.
  4. Output: Rows go to the default dataset (or another named store); binaries and snapshots often go to KV.
  5. Completion: the run settles in one of four terminal statuses: SUCCEEDED, FAILED, TIMED-OUT, or ABORTED. TIMING-OUT and ABORTING are 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 is application/json; set contentType for 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)​

StorageBest forTypical access
DatasetMany homogeneous records (products, posts, map pins)pushData, API dataset items
Key-value storeFiles, single JSON payloads, crawl statesetValue / getValue, named keys
Request queueCrawler 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?)​

  1. Reproducible: Versioned Docker images + logged inputs make “what ran?” answerable.
  2. Operational: Logging, retries, and scheduling are platform features, not one-off shell scripts.
  3. Shareable: Store publishing reuses the same interface for strangers running your Actor.
  4. 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​

AspectTraditional scriptActor
HostingYour server / laptopApify managed containers
ScalingYou provisionParallel runs per limits
StateCustom DB / filesKV + datasets + queues
ProxiesDIYFirst-class configuration
SchedulingCron on a VMBuilt-in tasks & cron
SharingGit onlyStore + input schema

Getting started​

  1. Scaffold with the CLI. See Build an Apify Actor.
  2. Define INPUT_SCHEMA.json early so API and Console users stay aligned.
  3. Push data incrementally with pushData instead of holding huge arrays in memory.
Frequently Asked Questions

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.