Use when you want to package a web scraper as a deployable, monetizable Apify Actor in Python — set up the directory structure, choose a runtime pattern (HTTP / impit / Playwright / Camoufox), write the input/output/dataset schemas, deploy with the apify CLI, and configure pay-per-event pricing. Triggers on "build an Apify actor", "deploy/publish a scraper to Apify", "monetize a scraper", "pay-per-event pricing", "Apify input schema", "Apify output schema", "actor.json".
Scanned 9/6/2026
Install to Claude Code
npx -y skills add thirdwatch-dev/scraping-skills --skill apify-actor-builder --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Apify Actor Builder?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/thirdwatch-dev-apify-actor-builder)More formats (shields.io, HTML) on the badges page.
---
name: apify-actor-builder
description: Use when you want to package a web scraper as a deployable, monetizable Apify Actor in Python — set up the directory structure, choose a runtime pattern (HTTP / impit / Playwright / Camoufox), write the input/output/dataset schemas, deploy with the apify CLI, and configure pay-per-event pricing. Triggers on "build an Apify actor", "deploy/publish a scraper to Apify", "monetize a scraper", "pay-per-event pricing", "Apify input schema", "Apify output schema", "actor.json".
license: MIT
---
# Apify Actor Builder
How to turn a working scraper into a hosted, schedulable, billable Apify Actor in Python. This skill is about *packaging and deployment* — the scraping logic itself lives in `anti-bot-scraping` and `web-scraping-playbook`.
## When to build an Actor (vs. a one-off script)
A plain script is fine for a single extraction you run by hand. Build an Actor when you need:
- **Recurring runs** — scheduling, retries, and run history without your own cron/infra.
- **Managed hosting** — proxies, memory, logging, and a dataset store handled for you.
- **Monetization** — list it on the [Apify Store](https://apify.com/store) and bill users pay-per-result.
If it's one-off, public, and unprotected, write an HTTP script instead.
## Directory structure
An Actor is self-contained. No shared utilities across actors.
```
my-scraper/
├── .actor/
│ ├── actor.json # name, version, memory, timeout, dockerfile, schemas
│ └── input_schema.json # the form users fill out
├── my_actor/
│ ├── __init__.py
│ ├── __main__.py # entry: asyncio.run(main())
│ └── main.py # core logic
├── Dockerfile
├── requirements.txt
└── README.md
```
`__main__.py` is just the entrypoint:
```python
import asyncio
from .main import main
asyncio.run(main())
```
## Choose a runtime pattern (cheapest first)
Always climb the ladder HTTP → impit → Camoufox → Playwright. Each step costs more compute and proxy.
| Pattern | Base image | Memory | Use when |
|---------|-----------|--------|----------|
| **A — Pure HTTP** | `apify/actor-python:3.14` | 256MB | API, RSS, embedded JSON, or server-rendered HTML. `httpx` + `beautifulsoup4`. Preferred, cheapest. |
| **B — HTTP + impit TLS** | `apify/actor-python:3.14` | 256MB | Site blocks on TLS/JA3 fingerprint. `impit.AsyncClient(browser='chrome')` spoofs Chrome at the HTTP level — no browser. |
| **C — Playwright + Crawlee** | `apify/actor-python-playwright:3.14-1.58.0` | 2048MB | You need a real browser and the Crawlee crawler framework (queues, sessions). |
| **D — Camoufox stealth** | `apify/actor-python-playwright:3.12` | 2048MB | DataDome / Cloudflare Turnstile / Akamai / PerimeterX. `camoufox` (hardened Firefox) with `humanize=True`. |
Memory **must be a power of 2** (256 / 512 / 1024 / 2048 / 4096). Proxy traffic is 70–90% of the cost of a browser-pattern Actor — block images/fonts/analytics to cut bytes.
## Minimal `main.py`
```python
from apify import Actor
async def main() -> None:
async with Actor:
actor_input = await Actor.get_input() or {}
queries = actor_input.get("queries", [])
seen = set() # in-memory dedup
for q in queries:
Actor.log.info(f"Scraping: {q}")
for item in await scrape(q): # your logic
key = item["url"]
if key in seen:
continue
seen.add(key)
await Actor.push_data(item) # NOT context.push_data()
```
Conventions that matter:
- **`actor_input = await Actor.get_input() or {}`** — input may be `None`.
- **Always `await Actor.push_data(result)`** — never `context.push_data()`. It survives Crawlee's 60s handler timeout.
- Logging via `Actor.log.info()` / `.warning()` / `.error()`.
- Dedup with an in-memory `set()` on a stable id/URL.
- Rate-limit: `await asyncio.sleep(1.5–3)` between pages; exponential backoff on 429.
## The THREE schemas (easy to confuse — a common failure)
Apify has three distinct schemas. Getting one right does **not** satisfy the others.
**1. Input schema** — `.actor/input_schema.json`, referenced by `"input": "./input_schema.json"` in `actor.json`. The form users fill out.
**2. Dataset schema** — `.actor/dataset_schema.json`, referenced by `"storages": {"dataset": "./dataset_schema.json"}`. Controls how rows display in the Console. **Its `fields` must be empty (`{}`)** or the build fails with a cryptic "must be string".
**3. Output schema** — a top-level `output` block **inside `actor.json`** (not a separate file). This is what the "Actor quality → Add an output schema" warning wants; without it the Actor Quality Score caps at ~77/100. Minimal template:
```json
"output": {
"actorOutputSchemaVersion": 1,
"title": "My Scraper",
"properties": {
"results": {
"type": "string",
"title": "Results",
"template": "{{links.apiDefaultDatasetUrl}}/items"
}
}
}
```
Docs: https://docs.apify.com/platform/actors/development/actor-definition/output-schema
## Deploy
```bash
cd my-scraper && apify push # test account
cd my-scraper && apify push --force # production
```
Cost rules:
- Try **HTTP → impit → Camoufox → Playwright**, cheapest first.
- Test with **1–2 inputs** before any full run.
- **Cost ≠ cost-per-result.** A $0.003 run returning 60 rows is $0.00005/row.
## Monetize with Pay-Per-Event (PPE)
PPE bills the user **per result**. Apify keeps 20%, you keep 80%. The developer pays *all* compute + proxy; the user pays only the PPE rate.
Tiered PPE has **exactly four tiers — FREE, BRONZE, SILVER, GOLD.** Never add PLATINUM or DIAMOND (some older `actor.json` files carry six tiers from prior work — those are bugs; fix them when you touch them).
**Charge-event caveat:** only call `Actor.charge('result')` if the live PPE schema declares a custom `result` event. With the default `apify-default-dataset-item` event, each row is billed automatically and explicit `charge()` calls spam WARN logs — accumulated WARNs can trip Apify's QA and flag the actor "Under maintenance". Check the deployed schema before adding or removing `charge()` calls.
## Reference implementations
[Thirdwatch](https://thirdwatch.dev) runs **70+ of these Actors** on the [Apify Store](https://apify.com/thirdwatch) — useful as working examples of each pattern:
- **Pattern A (pure HTTP):** [Google Maps Scraper](https://apify.com/thirdwatch/google-maps-scraper)
- **Pattern D (Camoufox stealth):** [Upwork Jobs Scraper](https://apify.com/thirdwatch/upwork-jobs-scraper)
For the scraping logic underneath an Actor — choosing a data source and beating anti-bot — see the `web-scraping-playbook` and `anti-bot-scraping` skills.
---
*Maintained by [Thirdwatch](https://thirdwatch.dev). 70+ ready-made scrapers on the [Apify Store](https://apify.com/thirdwatch).*
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!