references/actor-json.md
# Actor configuration (actor.json)
The `.actor/actor.json` file contains the Actor's configuration including metadata, schema references, and platform settings.
## Structure
```json
{
"actorSpecification": 1,
"name": "project-name",
"title": "Project Title",
"description": "Actor description",
"version": "0.0",
"meta": {
"templateId": "template-id",
"generatedBy": "<FILL-IN-TOOL-AND-MODEL>"
},
"inputSchema": "./input_schema.json",
"outputSchema": "./output_schema.json",
"storages": {
"dataset": "./dataset_schema.json"
},
"dockerfile": "../Dockerfile"
}
```
## Example
```json
{
"actorSpecification": 1,
"name": "project-cheerio-crawler-javascript",
"title": "Project Cheerio Crawler JavaScript",
"description": "Crawlee and Cheerio project in JavaScript.",
"version": "0.0",
"meta": {
"templateId": "js-crawlee-cheerio",
"generatedBy": "Claude Code with Claude Sonnet 4.5"
},
"inputSchema": "./input_schema.json",
"outputSchema": "./output_schema.json",
"storages": {
"dataset": "./dataset_schema.json"
},
"dockerfile": "../Dockerfile"
}
```
## Properties
- `actorSpecification` (integer, required) - Version of Actor specification (currently 1)
- `name` (string, required) - Actor identifier (lowercase, hyphens allowed)
- `title` (string, required) - Human-readable title displayed in UI
- `description` (string, optional) - Actor description for marketplace
- `version` (string, required) - Semantic version number
- `meta` (object, optional) - Metadata about Actor generation
- `templateId` (string) - ID of template used to create the Actor
- `generatedBy` (string) - Tool and model name that generated/modified the Actor (e.g., "Claude Code with Claude Sonnet 4.5")
- `inputSchema` (string, optional) - Path to input schema file
- `outputSchema` (string, optional) - Path to output schema file
`input` and `output` are the deprecated names of `inputSchema` and `outputSchema`. Use the current names, one key per schema.
- `storages` (object, optional) - Storage schema references
- `dataset` (string) - Path to dataset schema file
- `keyValueStore` (string) - Path to key-value store schema file
- `dockerfile` (string, optional) - Path to Dockerfile
- `usesStandbyMode` (boolean, optional) - Enable Standby mode (`true` = Actor runs as a persistent HTTP server). See [standby-mode.md](standby-mode.md) for details
- `webServerSchema` (string or object, optional) - Specify when using Standby mode. OpenAPI v3 schema for the Actor's HTTP endpoints. Path to schema file or inline object.
**Important:** Always fill in the `generatedBy` property with the tool and model you're currently using (e.g., "Claude Code with Claude Sonnet 4.5") to help Apify improve documentation.
references/actor-readme.md
# Actor README guidelines
The README is the Actor's landing page on Apify Store. It serves as SEO content, first impression, usage guide, and support resource. **Always generate a README.md when creating or deploying an Actor.**
## Required structure
Write in Markdown. Use H2 (`##`) for main sections (these form the table of contents) and H3 (`###`) for subsections. Do not use H1 — the Actor name is automatically used as H1.
### 1. What does [Actor name] do?
- 1-2 sentences explaining what the Actor does and doesn't do
- Include a link to the target website
- Mention keywords like "API" (e.g., "Instagram API alternative")
- Bold the most important terms
### 2. Why use [Actor name]? / Why scrape [target site]?
- Business use cases and benefits
- List main features and capabilities
- Highlight the Apify platform advantages: scheduling, API access, integrations, proxy rotation, and monitoring
### 3. What data can [Actor name] extract?
- Table showing main data fields the Actor outputs (field name, type, description)
- Don't list every field — focus on the most useful and understandable ones
### 4. How to scrape [target site]
- Numbered step-by-step tutorial (Google may pick these up as rich snippets)
- Include a link to blog tutorials if they exist
### 5. How much will it cost to scrape [target site]?
- Set pricing expectations based on the Actor's pricing model
- For pay-per-result: mention free tier limits and what larger plans offer
- For compute units: explain average data volume per dollar
- Cost-related questions rank well in Google search
### 6. Input
- Reference the input tab: "See the input tab for full configuration options"
- Explain any complex input fields or special formatting requirements
- Screenshot of the input schema is optional but helpful
### 7. Output
- Include: "You can download the dataset in various formats such as JSON, HTML, CSV, or Excel"
- Show a simplified JSON output example (2-3 items)
- If output is complex, show separate examples for different data types
### 8. Tips / Advanced options (if applicable)
- How to limit compute unit usage
- How to get more accurate results or improve speed
### 9. FAQ, Disclaimers, and Support
- Legal/scraping disclaimer (use this template and customize with the target site name):
> Our Actors are ethical and do not extract any private user data, such as email addresses, gender, or location. They only extract what the user has chosen to share publicly. We therefore believe that our Actors, when used for ethical purposes by Apify users, are safe. However, you should be aware that your results could contain personal data. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not scrape personal data unless you have a legitimate reason to do so. If you're unsure whether your reason is legitimate, consult your lawyers.
- Common troubleshooting tips
- Mention the Issues tab for feedback
- Link to API tab for programmatic access
- Use cases for the extracted data
## SEO best practices
- Include keywords naturally in H2/H3 headings (e.g., "How to scrape Instagram" not just "How to use")
- Target "People Also Ask" style questions as H3 headings
- Aim for at least 300 words total
- Embed a YouTube video URL if available (renders automatically as a player)
- Make images clickable with links
## Tone
- Match the README tone to the target audience skill level
- For no-code users: use plain language, avoid code blocks early on
- For developers: include technical details, code examples, and API references
- Be clear about what technical knowledge is needed to use the Actor
## Reference Actors
Before writing a README, review these top Actors on Apify Store for best practices on structure, tone, and content:
- [Instagram Scraper](https://apify.com/apify/instagram-scraper)
- [Google Maps Scraper](https://apify.com/compass/crawler-google-places)
## Key rules
- Always write the README as part of Actor development — do not skip this step
- The first 25% of the README is what most visitors read — put the most important info there
- Use emojis sparingly as bullet points to break up text
- Keep images compressed but good quality
- Use [Carbon](https://github.com/carbon-app/carbon) for code snippet screenshots if needed
references/input-schema.md
# Input schema
`.actor/input_schema.json` drives both validation and the Console form. The platform validates input against it before the Actor starts, so a field the code reads but the schema omits fails silently for users of the form. Full specification: https://docs.apify.com/actors/development/actor-definition/input-schema/specification/v1
```json
{
"title": "E-commerce Product Scraper Input",
"type": "object",
"schemaVersion": 1,
"properties": {
"startUrls": {
"title": "Start URLs",
"type": "array",
"description": "Category or product pages to start from",
"editor": "requestListSources",
"prefill": [{ "url": "https://example.com/category" }]
},
"maxRequestsPerCrawl": {
"title": "Max requests per crawl",
"type": "integer",
"description": "Maximum number of pages to scrape (0 = unlimited)",
"default": 1000,
"minimum": 0,
"sectionCaption": "Limits"
},
"locale": {
"title": "Locale",
"type": "string",
"description": "Language of the scraped content",
"editor": "select",
"default": "en",
"enum": ["cs", "en", "de"],
"enumTitles": ["Czech", "English", "German"]
},
"proxyConfiguration": {
"title": "Proxy configuration",
"type": "object",
"description": "Proxy settings for anti-bot protection",
"editor": "proxy",
"default": { "useApifyProxy": true }
}
},
"required": ["startUrls"]
}
```
Every field needs `title`, `type`, and `description`. Types are `string`, `integer`, `number`, `boolean`, `object`, `array`.
## default, prefill, required
- `default` is what the Actor receives when the caller omits the field, from the form, API, CLI, or scheduler alike.
- `prefill` only fills the form. It never reaches the Actor unless the user submits it, so it is the safe place for example URLs.
- `required` forces the user to enter a value. Pair it with `prefill` when helpful; pairing it with `default` is redundant.
- `nullable: true` allows `null` independently of `required`.
## Editors by type
| Type | Editors | Notes |
|------|---------|-------|
| string | `textfield`, `textarea`, `select`, `datepicker`, `javascript`, `python`, `fileupload`, `hidden` | `pattern`, `minLength`, `maxLength`. `select` needs `enum` or `enumSuggestedValues` (free text allowed), labelled by `enumTitles`. `datepicker` takes `dateType`: `absolute`, `relative`, or `absoluteOrRelative`. |
| integer, number | `number`, `hidden` | `minimum`, `maximum`, and `unit` for a display suffix such as `"seconds"`. |
| boolean | `checkbox`, `hidden` | `groupCaption` and `groupDescription` on the first checkbox group a run of them. |
| object | `json`, `proxy`, `schemaBased`, `hidden` | `properties`, `required`, `additionalProperties`, `minProperties`, `maxProperties`. `proxy` yields `{ useApifyProxy, apifyProxyGroups, proxyUrls }`. `schemaBased` renders sub-properties as form fields, one level deep. |
| array | `requestListSources`, `stringList`, `keyValue`, `globs`, `pseudoUrls`, `select`, `json`, `schemaBased`, `fileupload`, `hidden` | `items`, `minItems`, `maxItems`, `uniqueItems`. Multiselect is `select` with `items.enum`. `keyValue` and `stringList` take `placeholderKey` / `placeholderValue`. |
| resource (string or array) | `resourcePicker`, `textfield`, `hidden` | `resourceType`: `dataset`, `keyValueStore`, `requestQueue`, or `mcpConnector`, with `resourcePermissions` `["READ"]` or `["READ", "WRITE"]` for storages. |
## Other field properties
- `sectionCaption` and `sectionDescription` start a collapsible section that runs until the next caption.
- `isSecret: true` encrypts the value at rest. Allowed with `textfield`, `textarea`, `hidden` (strings) and `json`, `hidden` (objects, arrays). `Actor.getInput()` decrypts transparently; reading the `INPUT` record directly returns `ENCRYPTED_VALUE:...`.
- `errorMessage` maps a validation keyword to a custom message, worth it for regex `pattern` fields: `{ "pattern": "Must be a valid email address" }`.
- `example` shows a sample value on the Store page.
- Escape backslashes twice in `pattern`, since it lives inside a JSON string.
- `patternKey` and `patternValue` are deprecated; use `items` or `properties` with `pattern` on the individual field.
- The whole file is capped at 500 kB. Top-level `additionalProperties: false` rejects unknown input keys.
Run `apify validate-schema` after editing.
references/logging.md
# Actor logging reference
## JavaScript and TypeScript
**ALWAYS use `log` from the `apify` package for logging** (`import { Actor, log } from 'apify'`) - This logger contains critical security logic including censoring sensitive data (Apify tokens, API keys, credentials) to prevent accidental exposure in logs.
### Available log levels
The Apify logger provides the following methods for logging:
- `log.debug()` - Debug level logs (detailed diagnostic information)
- `log.info()` - Info level logs (general informational messages)
- `log.warning()` - Warning level logs (warning messages for potentially problematic situations)
- `log.warningOnce()` - Warning level logs (same warning message logged only once)
- `log.error()` - Error level logs (error messages for failures)
- `log.exception()` - Exception level logs (for exceptions with stack traces)
- `log.perf()` - Performance level logs (performance metrics and timing information)
- `log.deprecated()` - Deprecation level logs (warnings about deprecated code)
- `log.softFail()` - Soft failure logs (non-critical failures that don't stop execution, e.g., input validation errors, skipped items)
- `log.internal()` - Internal level logs (internal/system messages)
### Best practices
- Use `log.debug()` for detailed operation-level diagnostics (inside functions)
- Use `log.info()` for general informational messages (API requests, successful operations)
- Use `log.warning()` for potentially problematic situations (validation failures, unexpected states)
- Use `log.error()` for actual errors and failures
- Use `log.exception()` for caught exceptions with stack traces
## Python
**ALWAYS use `Actor.log` for logging** - This logger contains critical security logic including censoring sensitive data (Apify tokens, API keys, credentials) to prevent accidental exposure in logs.
### Available log levels
The Apify Actor logger provides the following methods for logging:
- `Actor.log.debug()` - Debug level logs (detailed diagnostic information)
- `Actor.log.info()` - Info level logs (general informational messages)
- `Actor.log.warning()` - Warning level logs (warning messages for potentially problematic situations)
- `Actor.log.error()` - Error level logs (error messages for failures)
- `Actor.log.exception()` - Exception level logs (for exceptions with stack traces)
### Best practices
- Use `Actor.log.debug()` for detailed operation-level diagnostics (inside functions)
- Use `Actor.log.info()` for general informational messages (API requests, successful operations)
- Use `Actor.log.warning()` for potentially problematic situations (validation failures, unexpected states)
- Use `Actor.log.error()` for actual errors and failures
- Use `Actor.log.exception()` for caught exceptions with stack traces
references/output-schemas.md
# Output schemas
Generate or update `dataset_schema.json`, `output_schema.json`, and `key_value_store_schema.json` in `.actor/`, then wire them into `actor.json`. These files tell Apify Console how to display run results. Work on the Actor named in the request, or the one in the current directory.
## 1. Discover what the code outputs
1. Read `.actor/actor.json`. Note any existing schema files, and any inline `storages.dataset` or `storages.keyValueStore` objects (they migrate into files in step 5).
2. Search the repository for other `.actor/*_schema.json` files. Match their description style, field naming, example format, view size, and JSON formatting. A template's `dataset_schema.json` with `"fields": {}` is a placeholder to replace, not a style to match.
3. Find every dataset write: `pushData(` in JS/TS, `push_data(` in Python.
4. Find every key-value store write: `setValue(` in JS/TS, `set_value(` in Python. Ignore the default `INPUT` key.
5. Find the output type: a TypeScript interface, or a Python TypedDict, dataclass, or Pydantic model. When one exists it is the canonical field list; derive the schema from it and cross-check against the code that fills the values.
Done when you can list every output field with its type, source location, and nullability, and every key-value store key pattern. Present that list to the user.
## 2. Write `dataset_schema.json`
```json
{
"actorSpecification": 1,
"fields": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"required": [],
"additionalProperties": true
},
"views": {
"overview": {
"title": "Overview",
"description": "Most important fields at a glance",
"transformation": { "fields": [] },
"display": { "component": "table", "properties": {} }
}
}
}
```
**`fields.properties` is the superset**: every field the Actor can output. Views select a subset of it for display.
Rules for every field:
| Rule | Detail |
|------|--------|
| `"type"` | Always present. AJV rejects `nullable` without `type` on the same field. A TypeScript union such as `Foo \| string` becomes `"type": ["object", "string"]`. |
| `"nullable": true` | On every field. Websites and APIs drop values without warning. `nullable` is an Apify extension to draft-07. |
| `"description"` and `"example"` | On every field. Examples are anonymized: `"exampleuser"`, `"Example Channel"`, ISO dates like `"2025-01-15T12:00:00.000Z"`, URLs in the platform's format with fake IDs of the right length. |
| `"required": []` and `"additionalProperties": true` | On the top-level `fields` object **and** on every nested object inside `properties`. The nested level is the one most often missed. |
Two patterns cover the rest:
```json
"title": {
"type": "string",
"description": "Title of the scraped item",
"nullable": true,
"example": "Example Item Title"
},
"authorInfo": {
"type": "object",
"description": "Information about the author",
"properties": {
"name": { "type": "string", "nullable": true },
"url": { "type": "string", "nullable": true }
},
"required": [],
"additionalProperties": true,
"nullable": true,
"example": { "name": "Example Author", "url": "https://example.com/author" }
}
```
Arrays add `"items": { "type": "string" }`; enums add `"enum": [...]`.
**Views.** `transformation.fields` lists the 8 to 12 fields a user wants at a glance, in column order. `display.properties` gives each one a `label` and a `format` from `text`, `number`, `date`, `link`, `boolean`, `image`, `array`, `object`. Transformation also accepts `unwind`, `flatten`, `omit`, `limit`, and `desc`.
## 3. Write `key_value_store_schema.json` (only when step 1 found store writes)
Group the writes by key pattern. A fixed key such as `"REPORT"` becomes a collection with `key`; a dynamic key such as `` `screenshot-${id}` `` becomes one with `keyPrefix`. Each collection uses exactly one of the two.
```json
{
"actorKeyValueStoreSchemaVersion": 1,
"title": "Scraped Files",
"description": "Downloaded files and screenshots",
"collections": {
"report": {
"title": "Report",
"description": "Final analysis report",
"key": "REPORT",
"contentTypes": ["application/json"]
},
"screenshots": {
"title": "Screenshots",
"description": "Page screenshots captured during scraping",
"keyPrefix": "screenshot-",
"contentTypes": ["image/png", "image/jpeg"]
}
}
}
```
`contentTypes` restricts MIME types. `jsonSchema` (draft-07) validates `application/json` records.
## 4. Write `output_schema.json`
```json
{
"actorOutputSchemaVersion": 1,
"title": "Output of the <Actor name>",
"description": "One sentence describing the output data",
"properties": {
"dataset": {
"type": "string",
"title": "Results",
"description": "Dataset containing all scraped data",
"template": "{{links.apiDefaultDatasetUrl}}/items"
}
}
}
```
Every property has `"type": "string"`; the Apify meta-validator accepts no other type here. When step 3 produced a store schema, add a second property with template `{{links.apiDefaultKeyValueStoreUrl}}/keys`.
Available template variables: `links.apiDefaultDatasetUrl`, `links.apiDefaultKeyValueStoreUrl`, `links.publicRunUrl`, `links.consoleRunUrl`, `links.apiRunUrl`, `links.containerRunUrl`, `run.defaultDatasetId`, `run.defaultKeyValueStoreId`.
## 5. Wire `actor.json`
```json
"outputSchema": "./output_schema.json",
"storages": {
"dataset": "./dataset_schema.json",
"keyValueStore": "./key_value_store_schema.json"
}
```
Include `keyValueStore` only when step 3 ran. Move any inline schema objects into the files and replace them with these path strings. When `actor.json` still has the deprecated `input` or `output` key, rename it to `inputSchema` or `outputSchema`.
## 6. Validate
Run `apify validate-schema`. It checks the structure of the input, dataset, and key-value store schemas, not the field rules below, and CLI 1.10 skips a schema referenced as `outputSchema`, so check `output_schema.json` against the list by hand. The work is done when every line holds:
- [ ] Every output field found in step 1 is in `fields.properties`.
- [ ] Every field has `type`, `nullable: true`, `description`, and an anonymized `example`.
- [ ] `required: []` and `additionalProperties: true` sit on the top-level `fields` object and on every nested object.
- [ ] Field names match the keys the code writes (camelCase or snake_case as the code has them).
- [ ] The overview view lists 8 to 12 fields with correct formats.
- [ ] Every `output_schema.json` property has `"type": "string"`.
- [ ] If a store schema exists, its collections cover every `setValue` / `set_value` call, each with `key` or `keyPrefix` but not both.
- [ ] `actor.json` references every schema file through `inputSchema`, `outputSchema`, and `storages`.
- [ ] `apify validate-schema` passes and reports the dataset and (if present) key-value store schemas, not only the input schema.
- [ ] Style matches the other schemas in the repository, and fields derive from the existing type definition when one exists.
Then show the user the schemas and report the files written, the field count, the overview fields, and any field whose type or nullability needs the user's confirmation.
references/standby-mode.md
# Actor Standby mode reference
## When to use Standby mode
Use Standby when the Actor must handle interactive, real-time HTTP requests — API endpoints, webhook receivers, real-time data lookups, MCP servers, or scraping APIs serving on-demand single-URL requests.
## Configuration
### actor.json
Set `usesStandbyMode: true` in `.actor/actor.json`:
```json
{
"actorSpecification": 1,
"name": "my-api-actor",
"title": "My API Actor",
"version": "0.0",
"usesStandbyMode": true,
"webServerSchema": "./openapi.json",
"meta": {
"generatedBy": "<FILL-IN-TOOL-AND-MODEL>"
},
"dockerfile": "../Dockerfile"
}
```
### OpenAPI Schema (`webServerSchema`)
Define an OpenAPI v3 schema describing the Actor's HTTP endpoints. This can be a file path (e.g., `"./openapi.json"`) or an inline object in `actor.json`. Ensure that the schema conforms to the OpenAPI spec.
**Why:** The schema is rendered as Swagger UI in the Standby tab of Apify Console and on the Actor's Apify Store page. This lets users browse endpoint documentation and try out the API directly from the browser.
The presence of `webServerSchema` also counts as a quality metric for Actor publication.
### Environment variables
| Variable | Description |
|----------|-------------|
| `ACTOR_WEB_SERVER_PORT` | Port the HTTP server must listen on. Access via SDK: JS `Actor.config.get('containerPort')`, Python `Actor.configuration.web_server_port`. Both default to 4321 locally |
| `ACTOR_STANDBY_URL` | The public Standby URL (stable across runs, format: `https://<username>--<actor-name>.apify.actor`) |
| `APIFY_META_ORIGIN` | Set to `STANDBY` when the Actor was launched in Standby mode |
## Readiness probe
The platform sends `GET /` requests with the header `x-apify-container-server-readiness-probe` to check server readiness. You MUST respond with HTTP 200. Keep the response lightweight.
## Authentication
Callers authenticate to Standby URLs via:
- **Bearer token** (recommended): `Authorization: Bearer <APIFY_TOKEN>`
- **Query parameter** (fallback): `?token=<APIFY_TOKEN>`
## Input handling
Standby Actors receive per-request input via HTTP query parameters or request body — NOT via `INPUT.json`. The traditional input schema (`input_schema.json`) is used for Actor initialization/configuration only, not per-request data.
## Complete examples
### JavaScript / TypeScript (Express)
```javascript
import { Actor, log } from 'apify';
import express from 'express';
await Actor.init();
const app = express();
app.use(express.json());
const port = Actor.config.get('containerPort');
// Readiness probe
app.get('/', (req, res) => {
if (req.headers['x-apify-container-server-readiness-probe']) {
return res.send('OK');
}
res.json({ status: 'Actor is running in Standby mode' });
});
// Example endpoint
app.get('/search', (req, res) => {
const { query } = req.query;
log.info('Handling search request', { query });
// ... handle request ...
res.json({ results: [] });
});
app.listen(port, () => log.info(`Listening on port ${port}`));
```
### Python (FastAPI)
```python
from apify import Actor
import uvicorn
from fastapi import FastAPI, Request
app = FastAPI()
@app.get('/')
async def root(request: Request):
if 'x-apify-container-server-readiness-probe' in request.headers:
return {'status': 'OK'}
return {'status': 'Actor is running in Standby mode'}
@app.get('/search')
async def search(query: str):
Actor.log.info('Handling search request', extra={'query': query})
# ... handle request ...
return {'results': []}
async def main():
async with Actor:
port = Actor.configuration.web_server_port
server = uvicorn.Server(uvicorn.Config(app, host='0.0.0.0', port=port))
await server.serve()
if __name__ == '__main__':
import asyncio
asyncio.run(main())
```
## Rules
- **NEVER disable standby mode** (`usesStandbyMode: false`) in `.actor/actor.json` without explicit user permission
- **NEVER call `Actor.exit()`** after handling a request — the server must stay alive
- **ALWAYS listen on the SDK-provided port**, not a hardcoded value
- **ALWAYS implement the readiness probe** at `GET /`
- **Standby Actor READMEs** must document: available endpoints, HTTP methods, request/response schemas, authentication (`Authorization: Bearer <token>`), and example calls.
## Testing
Start the server with `apify run`, as for any Actor. It sets up the Apify environment and serves on port 4321, but it does not simulate the Standby load balancer or send the readiness probe, so exercise both yourself:
1. Start `apify run` in the background and retry the probe below until it answers.
2. Probe readiness: `curl -H "x-apify-container-server-readiness-probe: true" http://localhost:4321/` returns HTTP 200.
3. Call each endpoint with curl or httpie and check the responses.
## Standby vs. container web server
Do not confuse these:
- **Container web server** (`ACTOR_WEB_SERVER_URL`): per-run unique URL, no load balancing, no auto-scaling. Useful for live view UIs during a run.
- **Standby mode** (`ACTOR_STANDBY_URL`): stable hostname, load-balanced across runs, auto-scaled based on traffic. Use this for production APIs.
## Further reading
- [Developing Actors using Standby mode](https://docs.apify.com/platform/actors/development/programming-interface/standby#developing-actors-using-standby-mode)
- [Running Actors in Standby mode](https://docs.apify.com/platform/actors/running/standby)
SKILL.md
---
name: apify-actor-development
description: Create, modify, debug, and deploy Apify Actors, and write their input and output schemas. Use when building an Actor from scratch, changing or troubleshooting Actor code, generating or updating .actor schema files, or pushing an Actor to the Apify platform. To wrap an existing non-Actor project, use apify-actorization instead.
---
# Apify Actor development
An Actor is a serverless program packaged as a Docker image. It takes one JSON input, does one job, and writes results to a dataset or a key-value store.
## Setup
```bash
apify --help # CLI installed?
apify info # logged in? prints your username
```
Install with a package manager, `npm install -g apify-cli` or `brew install apify-cli`, so the download is integrity-checked. Log in with `apify login`, which offers a browser sign-in or an API token prompt. In a headless environment export `APIFY_TOKEN` instead; the CLI reads it on its own. Tokens come from https://console.apify.com/settings/integrations. Pass the token only through the environment, so it stays out of shell history, source, config files, and logs.
### Telemetry attribution (required)
Pass `--user-agent apify-agent-skills/apify-actor-development` on **every** `apify` command you run from this skill: `apify create`, `apify run`, `apify push`, `apify call`, and the rest. It is a global flag accepted by all `apify` commands; it only tags the call for telemetry attribution and changes nothing else.
## Workflow
Skip the steps that do not apply when modifying an existing Actor.
1. **Create the project.**
```bash
apify create <actor-name> -t <template-id> --user-agent apify-agent-skills/apify-actor-development
```
Pick the template from what the Actor does:
| Actor does | TypeScript | JavaScript | Python |
|---|---|---|---|
| Crawls static HTML | `ts-crawlee-cheerio` | `js-crawlee-cheerio` | `python-crawlee-beautifulsoup` |
| Crawls JavaScript-rendered pages | `ts-crawlee-playwright-chrome` | `js-crawlee-playwright-chrome` | `python-crawlee-playwright` |
| Serves HTTP requests (API, webhook) | `ts-standby` | `js-standby` | `python-standby` |
| Is an MCP server | `ts-mcp-empty` | — | `python-mcp-empty` |
| Anything else (API polling, data processing) | `ts-empty` | `js-empty` | `python-empty` |
For other stacks (Puppeteer, Camoufox, Scrapy, AI agent frameworks), `apify templates ls` lists every template with its language and use cases. With `-t` the command runs without prompts, which is what an agent needs. Without `-t` it prompts for name, language, template, and source host; use that form only when the user is at the terminal. Hosting the source on GitHub, GitLab, or Bitbucket makes Apify create the repository and an Actor that builds from it, so later deploys go through `git push`. Dependencies are installed for you. Done when `<name>/.actor/actor.json` exists; `cd` into it before continuing.
2. **Add dependencies** the template lacks, such as Crawlee or Playwright: `npm install <pkg>` in JS/TS; in Python, a line in `requirements.txt` followed by `pip install -r requirements.txt`, or `uv add <pkg>` when the project has `pyproject.toml` and `uv.lock`. Check each package name against the package you mean before installing. Pin exact versions and commit the lockfile (`package-lock.json`, `uv.lock`, or `pkg==1.2.3` lines in `requirements.txt`).
3. **Implement** in `src/main.js`, `src/main.ts`, or `my_actor/main.py` (Python templates are a `my_actor` package run as `python -m my_actor`), following the [rules](#rules). Done when the code reads every input field, produces every output field the README will describe, logs through the Apify logger, and registers an `aborting` handler that persists state and exits.
4. **Write the input schema** in `.actor/input_schema.json` (see [references/input-schema.md](references/input-schema.md)). Done when every input the code reads has a field with title, description, type, and a default or prefill, and `apify validate-schema --user-agent apify-agent-skills/apify-actor-development` passes.
5. **Write the output schemas**: `dataset_schema.json`, `output_schema.json`, and `key_value_store_schema.json` when the code stores files. Follow [references/output-schemas.md](references/output-schemas.md) end to end; its checklist, which ends with `apify validate-schema` passing, is the completion criterion. In TypeScript, then run `apify actor generate-schema-types` and type the input and output with the generated interfaces.
6. **Configure `.actor/actor.json`** (see [references/actor-json.md](references/actor-json.md)). Set `meta.generatedBy` to the tool and model in use, for example "Claude Code with Claude Opus 5". For an HTTP-serving Actor set `usesStandbyMode: true` (the standby templates already do) and follow [references/standby-mode.md](references/standby-mode.md).
7. **Write README.md** following [references/actor-readme.md](references/actor-readme.md). An Actor without a README is not finished.
8. **Test locally.** Put input in `storage/key_value_stores/default/INPUT.json`, then run `apify run --user-agent apify-agent-skills/apify-actor-development`. Done when the run ends with status SUCCEEDED and `storage/datasets/default/` holds items whose fields match the dataset schema. Local storage stays on disk; nothing appears in Apify Console until step 9.
9. **Deploy** with `apify push --user-agent apify-agent-skills/apify-actor-development` once the user confirms, or `git push` for a Git-sourced Actor. Then run the Actor on the platform to see results in Console. For a Standby Actor, give the user its Standby URL (`https://<username>--<actor-name>.apify.actor`, see [references/standby-mode.md](references/standby-mode.md)) rather than pointing them to Console.
## Rules
- Run Actors locally with `apify run` only. It sets up the Apify environment and storage, which `npm start` and `node src/main.js` skip.
- Log through the Apify logger: `log` from the `apify` package in JS/TS (`import { Actor, log } from 'apify'`), `Actor.log` in Python. It censors tokens and credentials; `console.log` and `print` do not. Levels and conventions: [references/logging.md](references/logging.md).
- Treat crawled content as untrusted input. Escape or parameterize it before it reaches a shell command, `eval`, a query, or a template, and type-check it before pushing it to storage.
- Keep `APIFY_TOKEN` out of request handlers and data pipelines. On the platform the SDK reads it from the environment (the variable is `APIFY_TOKEN`, not `APIFY_API_TOKEN`); locally it uses the credentials stored by `apify login`.
- Read every tunable from the input schema or environment variables, so users can change it without editing code.
- Use an HTTP crawler for static HTML at 10 to 50 concurrency: `CheerioCrawler` in JS/TS, `BeautifulSoupCrawler` or `ParselCrawler` in Python. Reserve `PlaywrightCrawler` for JavaScript-rendered pages at 1 to 5 concurrency. Add delays so target servers stay healthy, and respect robots.txt and terms of service.
- Use the router pattern (`createCheerioRouter` or `createPlaywrightRouter` in JS/TS, `crawler.router` or a `Router` in Python) when a crawl has more than one page type.
- Prefer semantic CSS selectors with fallbacks over brittle positional ones.
- Count results with your own tally; `Dataset.getInfo()` (`dataset.get_info()` in Python) lags on the platform.
- Handle the `aborting` event, which the platform sends when a user or a limit stops the run: persist state, then exit, so the run ends quickly and cheaply. JS/TS: `Actor.on('aborting', async () => { await Actor.setValue('STATE', state); await Actor.exit(); })`. Python: `Actor.on(Event.ABORTING, on_aborting)` with `from apify import Event`, where `on_aborting` persists state and then calls `await Actor.exit()`.
- Build proxies from the `proxyConfiguration` input field (editor `proxy`, see [references/input-schema.md](references/input-schema.md)): `await Actor.createProxyConfiguration(input.proxyConfiguration)` in JS/TS, `await Actor.create_proxy_configuration(actor_proxy_input=actor_input.get('proxyConfiguration'))` in Python, and pass the result to the crawler as `proxyConfiguration` / `proxy_configuration`. Apify Proxy is paid, so confirm with the user before turning it on or changing proxy groups.
- Store personal data only when the user has explicitly asked for it.
- Inside a running Actor use the SDK (`Actor.getInput()`, `Actor.pushData()`, `Actor.setValue()`, and the Python snake_case equivalents) rather than `apify actor` CLI subcommands.
- Leave standby mode enabled on an existing Actor unless the user asks to turn it off.
## Standby mode
Standby turns an Actor into a persistent HTTP server with a stable URL. Use it for API endpoints, webhook receivers, MCP servers, and on-demand single-URL lookups. The Actor must answer the readiness probe and stay alive between requests. Configuration, examples, and local testing: [references/standby-mode.md](references/standby-mode.md).
## Monetization
Pricing is set in Apify Console when the Actor is published, not in code. Under pay-per-event, charge each custom event with `await Actor.charge({ eventName: 'result', count })` in JS/TS or `await Actor.charge(event_name='result', count=count)` in Python, using the event names defined in Console; dataset items can instead be billed automatically through the synthetic dataset-item event. Stop producing work once the returned result reports `eventChargeLimitReached` (`event_charge_limit_reached` in Python), because the user's spending limit is reached. The README's cost section describes whichever model the Actor uses. Details: https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event
## Calling other Actors
Search the Store before building from scratch; a dedicated Actor often exists.
```bash
apify actors search "<query>" --user-agent apify-agent-skills/apify-actor-development
apify actors info <actor> --readme --user-agent apify-agent-skills/apify-actor-development
apify actors info <actor> --input --user-agent apify-agent-skills/apify-actor-development
apify call <actor> --input '{"startUrls":[{"url":"https://example.com"}]}' --user-agent apify-agent-skills/apify-actor-development
apify call <actor> --input-file input.json --user-agent apify-agent-skills/apify-actor-development
```
Input is one JSON object. Quote inline JSON; use `--input-file` for anything complex.
## Less obvious commands
```bash
# Append --user-agent apify-agent-skills/apify-actor-development to each of these too.
apify secrets add <name> <value> # reference from actor.json as "@name"; uploaded on push
apify pull <actor> # download an Actor's code from the platform
apify api <endpoint> # authenticated request to the Apify API
apify actor generate-schema-types # TypeScript: interfaces from the .actor schemas, into src/__generated__/actor/
apify <command> --help
```
## Documentation
- With the Apify MCP server (`https://mcp.apify.com/?tools=docs`): `search-apify-docs` and `fetch-apify-docs`.
- [docs.apify.com/llms.txt](https://docs.apify.com/llms.txt) and [llms-full.txt](https://docs.apify.com/llms-full.txt), Apify platform.
- [crawlee.dev/llms.txt](https://crawlee.dev/llms.txt) and [llms-full.txt](https://crawlee.dev/llms-full.txt), Crawlee.
- [Actor whitepaper](https://raw.githubusercontent.com/apify/actor-whitepaper/refs/heads/master/README.md), the full Actor specification.
- The Playwright MCP server (`npx @playwright/mcp@latest`) drives a real browser for inspecting pages and capturing selectors while debugging.