1# ceo-founder-email-finder-by-domain: contract for agents and integrators
2
3## Input
4`domain` (required), `maxCandidates` (0 = all, hard max 50, default 50), `minLeadScore` (0-100, default 0), `outputFormat` (`default` | `compatible`). Advanced (hidden in the form, API only): `maxChargeUsd` (number >= 0, default 0 = no extra cap; the run stops with status `max_charge_reached` and one free explanation row when charges reach it), `sourceTag` (string, max 100; copied to every result row and to `OUTPUT`).
5Example: `{"domain": "example.com", "minLeadScore": 60}`
6
7## Output
8Dataset row fields: `inputDomain`, `fullName`, `title`, `contactType` (ceo | founder | leadership), `country`, `location`, `linkedinUrl`, `workEmail`, `leadScore`, `retrievedAt`, `scrapedAt`, `inputQuery`, `matchMethod`, `confidence`, `sourceTag` (only when set). Null fields are stripped. Stop conditions end the run SUCCEEDED with one explanation row (`status`, `message`) and no charge. The `OUTPUT` record in the key-value store holds the counts, per-input breakdown and duration. Error details are in `errors-*` records.
9
10## Match fields
11`matchMethod`: `domain_matched_work_email` (normal rows: the work email domain equals the requested domain), `title_keyword` (buying-committee rows), `executive_fallback` (fallback rows). `confidence`: `high` only for `domain_matched_work_email` rows whose email came back with status verified or deliverable; otherwise `medium`. The upstream data read today carries no email status, so rows are currently `medium`. Committee and fallback rows are capped at `medium`.
12
13## Billing
14One named event, `contact`, is charged per leadership contact pushed to the dataset. Explanation rows (bad input, free-plan limit (`free_plan_limit`), no match, rows below `minLeadScore`) are pushed without an event and are free. Failed lookups are free. Prices are on the Pricing tab. Free Apify plans get up to 100 results per run (`src/free-plan.js`, checked in `emit` in main.js); at the cap the run stops and adds one free `free_plan_limit` row. Paid plans are uncapped.
15
16## Lead score
17`leadScore` is a whole number from 0 to 100: workEmail on the input domain 40 + title 25 (CEO or founder) or 20 (President, Managing Partner, Chair) + linkedinUrl present 10 + location or country present 5 + network size 20 (followers + connections: 500 or more = 20, 100 or more = 10, below 100 = 0). Rows below `minLeadScore` are not pushed and not charged.
18
19## Compatible output
20`outputFormat: "compatible"` keeps every default field and adds: `firstName`, `lastName` (split from `fullName`), `email` (= `workEmail`), `jobTitle` and `headline` (= `title`), `companyDomain` (= `inputDomain`), `profileUrl` (= `linkedinUrl`).
21
22## Buying committee
23Input `targetPersonas` (array, 1-5 of `founder_ceo`, `sales_leader`, `marketing_leader`, `ops_logistics`, `procurement`, `finance_cfo`, `it_security`, `hr_people`; order = priority) turns on committee mode. Empty or absent = the normal search. Title keywords per preset:
24
25
26|---|---|
27| `founder_ceo` | founder, co-founder, CEO, chief executive, owner, managing director |
28| `sales_leader` | sales, revenue, business development, CRO, commercial |
29| `marketing_leader` | marketing, CMO, growth, brand, demand generation |
30| `ops_logistics` | operations, COO, logistics, supply chain, fulfilment |
31| `procurement` | procurement, purchasing, sourcing, buyer |
32| `finance_cfo` | finance, financial, CFO, controller, treasurer |
33| `it_security` | CTO, CIO, CISO, information technology, security, IT |
34| `hr_people` | HR, human resources, people, talent, CHRO, CPO |
35
36Output: one row per filled persona, same `inputDomain`, plus `targetPersona` and `personaSlot` (1-based position in your list). A persona with no match returns no row; `OUTPUT.missedPersonas` lists them. `executiveFallback` (bool, default true): an unfilled persona returns the most senior reachable leader, with `title` = their real role and `fellBackFrom` = the requested persona. Each returned row is one `contact` event, fallback rows included. A person is never returned twice. Profile lookups are capped by the max-results input.
37Example: `{"domain": "example.com", "targetPersonas": ["sales_leader", "finance_cfo", "it_security"], "executiveFallback": true}`
38
39## Chaining
40Chain with `executive-email-finder-by-domain` (C-suite, VP, Director at the same domain) and `company-firmographics-by-domain` (firmographics for the same domain).
41
42## Example API call
43`curl -X POST "https://api.apify.com/v2/acts/b2bdata~ceo-founder-email-finder-by-domain/run-sync-get-dataset-items?token=<APIFY_TOKEN>" -H "Content-Type: application/json" -d '{"domain": "example.com", "minLeadScore": 60}'`
44
45## Add-ons
46None. The data source returns no extra data worth a separate add-on, so there is one event only.
47
48---
49
50# Apify Actors development guide (generic)
51
52Important: Before you begin, fill in the `generatedBy` property in the meta section of `.actor/actor.json`. Replace it with the tool and model you're currently using, such as "Claude Code with Claude Sonnet 4.5". This helps Apify monitor and improve AGENTS.md for specific AI tools and models.
53
54## What are Apify Actors?
55
56- Actors are serverless programs that run in the cloud. They're inspired by the UNIX philosophy - programs that do one thing well and can be easily combined to build complex systems.
57- Actors are programs packaged as Docker images that run in isolated containers
58
59## Core Concepts
60
61- Accept well-defined JSON input
62- Perform isolated tasks (web scraping, automation, data processing)
63- Produce structured JSON output to datasets and/or store data in key-value stores
64- Can run from seconds to hours or even indefinitely
65- Persist state and can be restarted
66
67## Do
68
69- accept well-defined JSON input and produce structured JSON output
70- use Apify SDK (`apify`) for code running ON Apify platform
71- validate input early with proper error handling and fail gracefully
72- use CheerioCrawler for static HTML content (10x faster than browsers)
73- use PlaywrightCrawler only for JavaScript-heavy sites and dynamic content
74- use router pattern (createCheerioRouter/createPlaywrightRouter) for complex crawls
75- implement retry strategies with exponential backoff for failed requests
76- use proper concurrency settings (HTTP: 10-50, Browser: 1-5)
77- set sensible defaults in `.actor/input_schema.json` for all optional fields
78- set up output schema in `.actor/output_schema.json`
79- clean and validate data before pushing to dataset
80- use semantic CSS selectors and fallback strategies for missing elements
81- respect robots.txt, ToS, and implement rate limiting with delays
82- check which tools (cheerio/playwright/crawlee) are installed before applying guidance
83- use `apify/log` package for logging (censors sensitive data)
84- implement readiness probe handler for standby Actors
85- handle the `aborting` event to gracefully shut down when Actor is stopped
86
87## Don't
88
89- do not rely on `Dataset.getInfo()` for final counts on Cloud platform
90- do not use browser crawlers when HTTP/Cheerio works (massive performance gains with HTTP)
91- do not hard code values that should be in input schema or environment variables
92- do not skip input validation or error handling
93- do not overload servers - use appropriate concurrency and delays
94- do not scrape prohibited content or ignore Terms of Service
95- do not store personal/sensitive data unless explicitly permitted
96- do not use deprecated options like `requestHandlerTimeoutMillis` on CheerioCrawler (v3.x)
97- do not use `additionalHttpHeaders` - use `preNavigationHooks` instead
98- do not assume that local storage is persistent or automatically synced to Apify Console - when running locally with `apify run`, the `storage/` directory is local-only and is NOT pushed to the Cloud
99- do not disable standby mode (`usesStandbyMode: false`) without explicit permission
100
101## Logging
102
103- **ALWAYS use the `apify/log` package for logging** - This package contains critical security logic including censoring sensitive data (Apify tokens, API keys, credentials) to prevent accidental exposure in logs
104
105### Available Log Levels in `apify/log`
106
107The Apify log package provides the following methods for logging:
108
109- `log.debug()` - Debug level logs (detailed diagnostic information)
110- `log.info()` - Info level logs (general informational messages)
111- `log.warning()` - Warning level logs (warning messages for potentially problematic situations)
112- `log.warningOnce()` - Warning level logs (same warning message logged only once)
113- `log.error()` - Error level logs (error messages for failures)
114- `log.exception()` - Exception level logs (for exceptions with stack traces)
115- `log.perf()` - Performance level logs (performance metrics and timing information)
116- `log.deprecated()` - Deprecation level logs (warnings about deprecated code)
117- `log.softFail()` - Soft failure logs (non-critical failures that don't stop execution, e.g., input validation errors, skipped items)
118- `log.internal()` - Internal level logs (internal/system messages)
119
120**Best practices:**
121
122- Use `log.debug()` for detailed operation-level diagnostics (inside functions)
123- Use `log.info()` for general informational messages (API requests, successful operations)
124- Use `log.warning()` for potentially problematic situations (validation failures, unexpected states)
125- Use `log.error()` for actual errors and failures
126- Use `log.exception()` for caught exceptions with stack traces
127
128## Graceful Abort Handling
129
130Handle the `aborting` event to terminate the Actor quickly when stopped by user or platform, minimizing costs especially for PPU/PPE+U billing.
131
132```javascript
133import { setTimeout } from 'node:timers/promises';
134
135Actor.on('aborting', async () => {
136 // Persist any state, do any cleanup you need, and terminate the Actor using `await Actor.exit()` explicitly as soon as possible
137 // This will help ensure that the Actor is doing best effort to honor any potential limits on costs of a single run set by the user
138 // Wait 1 second to allow Crawlee/SDK useState and other state persistence operations to complete
139 // This is a temporary workaround until SDK implements proper state persistence in the aborting event
140 await setTimeout(1000);
141 await Actor.exit();
142});
143```
144
145## Standby Mode
146
147- **NEVER disable standby mode (`usesStandbyMode: false`) in `.actor/actor.json` without explicit permission** - Actor Standby mode solves this problem by letting you have the Actor ready in the background, waiting for the incoming HTTP requests. In a sense, the Actor behaves like a real-time web server or standard API server instead of running the logic once to process everything in batch. Always keep `usesStandbyMode: true` unless there is a specific documented reason to disable it
148- **ALWAYS implement readiness probe handler for standby Actors** - Handle the `x-apify-container-server-readiness-probe` header at GET / endpoint to ensure proper Actor lifecycle management
149
150You can recognize a standby Actor by checking the `usesStandbyMode` property in `.actor/actor.json`. Only implement the readiness probe if this property is set to `true`.
151
152### Readiness Probe Implementation Example
153
154```javascript
155// Apify standby readiness probe at root path
156app.get('/', (req, res) => {
157 res.writeHead(200, { 'Content-Type': 'text/plain' });
158 if (req.headers['x-apify-container-server-readiness-probe']) {
159 res.end('Readiness probe OK\n');
160 } else {
161 res.end('Actor is ready\n');
162 }
163});
164```
165
166Key points:
167
168- Detect the `x-apify-container-server-readiness-probe` header in incoming requests
169- Respond with HTTP 200 status code for both readiness probe and normal requests
170- This enables proper Actor lifecycle management in standby mode
171
172## Commands
173
174```bash
175# Bootstrap & local development
176apify create [name] # Create new Actor project from a template
177apify init # Initialize Actor in current directory
178apify run # Run Actor locally with simulated platform env
179apify run --purge # Run after clearing previous local storage
180apify validate-schema # Validate .actor/input_schema.json
181
182# Authentication & account
183apify login # Authenticate account (token stored in ~/.apify)
184apify logout # Remove stored credentials
185apify info # Print currently authenticated account info
186
187# Deployment & remote execution
188apify push # Deploy Actor to platform per .actor/actor.json
189apify pull <actor> # Download Actor code from the platform
190apify call <actor> # Execute Actor remotely on the platform
191apify actors build <actor> # Create a new build of an Actor
192apify runs ls # List recent runs
193
194# Discovery (search Apify Store for community Actors)
195apify actors search "<query>" --user-agent <your-agent-name>
196apify actors info <actor> # Details about a specific Actor
197
198# Secrets (referenced from actor.json via "@mySecret")
199apify secrets add <name> <value> # Store a secret locally; uploaded on push
200apify secrets ls # List stored secret keys
201
202# Direct API access
203apify api <endpoint> # Authenticated HTTP request to Apify API
204
205# Help
206apify help # List all commands
207apify <command> --help # Detailed help for a specific command
208```
209
210Note: If no dedicated Actor exists for your target, search Apify Store for community options with `apify actors search "<query>" --user-agent <your-agent-name>` before building from scratch.
211
212Tip: Inside a running Actor, prefer the SDK (`Actor.getInput()`, `Actor.pushData()`, `Actor.setValue()`) over the equivalent `apify actor` runtime subcommands.
213
214## Apify Platform Environment
215
216When the Actor runs on the Apify platform, the API token is automatically available via the `APIFY_TOKEN` environment variable (note: the variable is `APIFY_TOKEN`, not `APIFY_API_TOKEN`). The Apify SDK reads it automatically, so you do not need to pass it explicitly. Locally, run `apify login` once and the SDK will use your stored credentials.
217
218## Safety and Permissions
219
220Allowed without prompt:
221
222- read files with `Actor.getValue()`
223- push data with `Actor.pushData()`
224- set values with `Actor.setValue()`
225- enqueue requests to RequestQueue
226- run locally with `apify run`
227
228Ask first:
229
230- npm/pip package installations
231- apify push (deployment to cloud)
232- proxy configuration changes (requires paid plan)
233- Dockerfile changes affecting builds
234- deleting datasets or key-value stores
235
236## Project Structure
237
238.actor/
239├── actor.json # Actor config: name, version, env vars, runtime settings
240├── input_schema.json # Input validation & Console form definition
241└── output_schema.json # Specifies where an Actor stores its output
242src/
243└── main.js # Actor entry point and orchestrator
244storage/ # Local-only storage for development (NOT synced to Cloud)
245├── datasets/ # Output items (JSON objects)
246├── key_value_stores/ # Files, config, INPUT
247└── request_queues/ # Pending crawl requests
248Dockerfile # Container image definition
249AGENTS.md # AI agent instructions (this file)
250
251## Local vs Cloud Storage
252
253When running locally with `apify run`, the Apify SDK emulates Cloud storage APIs using the local `storage/` directory. This local storage behaves differently from Cloud storage:
254
255- **Local storage is NOT persistent** - The `storage/` directory is meant for local development and testing only. Data stored there (datasets, key-value stores, request queues) exists only on your local disk.
256- **Local storage is NOT automatically pushed to Apify Console** - Running `apify run` does not upload any storage data to the Apify platform. The data stays local.
257- **Each local run may overwrite previous data** - The local `storage/` directory is reused between runs, but this is local-only behavior, not Cloud persistence.
258- **Cloud storage only works when running on Apify platform** - After deploying with `apify push` and running the Actor in the Cloud, storage calls (`Actor.pushData()`, `Actor.setValue()`, etc.) interact with real Apify Cloud storage, which is then visible in the Apify Console.
259- **To verify Actor output, deploy and run in Cloud** - Do not rely on local `storage/` contents as proof that data will appear in the Apify Console. Always test by deploying (`apify push`) and running the Actor on the platform.
260
261## Actor Input Schema
262
263The input schema defines the input parameters for an Actor. It's a JSON object comprising various field types supported by the Apify platform.
264
265### Structure
266
267```json
268{
269 "title": "<INPUT-SCHEMA-TITLE>",
270 "type": "object",
271 "schemaVersion": 1,
272 "properties": {/* define input fields here */},
273 "required": []
274}
275```
276
277### Example
278
279```json
280{
281 "title": "E-commerce Product Scraper Input",
282 "type": "object",
283 "schemaVersion": 1,
284 "properties": {
285 "startUrls": {
286 "title": "Start URLs",
287 "type": "array",
288 "description": "URLs to start scraping from (category pages or product pages)",
289 "editor": "requestListSources",
290 "default": [{ "url": "https://example.com/category" }],
291 "prefill": [{ "url": "https://example.com/category" }]
292 },
293 "followVariants": {
294 "title": "Follow Product Variants",
295 "type": "boolean",
296 "description": "Whether to scrape product variants (different colors, sizes)",
297 "default": true
298 },
299 "maxRequestsPerCrawl": {
300 "title": "Max Requests per Crawl",
301 "type": "integer",
302 "description": "Maximum number of pages to scrape (0 = unlimited)",
303 "default": 1000,
304 "minimum": 0
305 },
306 "proxyConfiguration": {
307 "title": "Proxy Configuration",
308 "type": "object",
309 "description": "Proxy settings for anti-bot protection",
310 "editor": "proxy",
311 "default": { "useApifyProxy": false }
312 },
313 "locale": {
314 "title": "Locale",
315 "type": "string",
316 "description": "Language/country code for localized content",
317 "default": "cs",
318 "enum": ["cs", "en", "de", "sk"],
319 "enumTitles": ["Czech", "English", "German", "Slovak"]
320 }
321 },
322 "required": ["startUrls"]
323}
324```
325
326## Actor Output Schema
327
328The Actor output schema builds upon the schemas for the dataset and key-value store. It specifies where an Actor stores its output and defines templates for accessing that output. Apify Console uses these output definitions to display run results.
329
330### Structure
331
332```json
333{
334 "actorOutputSchemaVersion": 1,
335 "title": "<OUTPUT-SCHEMA-TITLE>",
336 "properties": {/* define your outputs here */}
337}
338```
339
340### Example
341
342```json
343{
344 "actorOutputSchemaVersion": 1,
345 "title": "Output schema of the files scraper",
346 "properties": {
347 "files": {
348 "type": "string",
349 "title": "Files",
350 "template": "{{links.apiDefaultKeyValueStoreUrl}}/keys"
351 },
352 "dataset": {
353 "type": "string",
354 "title": "Dataset",
355 "template": "{{links.apiDefaultDatasetUrl}}/items"
356 }
357 }
358}
359```
360
361### Output Schema Template Variables
362
363- `links` (object) - Contains quick links to most commonly used URLs
364- `links.publicRunUrl` (string) - Public run url in format `https://console.apify.com/view/runs/:runId`
365- `links.consoleRunUrl` (string) - Console run url in format `https://console.apify.com/actors/runs/:runId`
366- `links.apiRunUrl` (string) - API run url in format `https://api.apify.com/v2/actor-runs/:runId`
367- `links.apiDefaultDatasetUrl` (string) - API url of default dataset in format `https://api.apify.com/v2/datasets/:defaultDatasetId`
368- `links.apiDefaultKeyValueStoreUrl` (string) - API url of default key-value store in format `https://api.apify.com/v2/key-value-stores/:defaultKeyValueStoreId`
369- `links.containerRunUrl` (string) - URL of a webserver running inside the run in format `https://<containerId>.runs.apify.net/`
370- `run` (object) - Contains information about the run same as it is returned from the `GET Run` API endpoint
371- `run.defaultDatasetId` (string) - ID of the default dataset
372- `run.defaultKeyValueStoreId` (string) - ID of the default key-value store
373
374## Dataset Schema Specification
375
376The dataset schema defines how your Actor's output data is structured, transformed, and displayed in the Output tab in the Apify Console.
377
378### Example
379
380Consider an example Actor that calls `Actor.pushData()` to store data into dataset:
381
382```javascript
383import { Actor } from 'apify';
384// Initialize the JavaScript SDK
385await Actor.init();
386
387/**
388 * Actor code
389 */
390await Actor.pushData({
391 numericField: 10,
392 pictureUrl: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png',
393 linkUrl: 'https://google.com',
394 textField: 'Google',
395 booleanField: true,
396 dateField: new Date(),
397 arrayField: ['#hello', '#world'],
398 objectField: {},
399});
400
401// Exit successfully
402await Actor.exit();
403```
404
405To set up the Actor's output tab UI, reference a dataset schema file in `.actor/actor.json`:
406
407```json
408{
409 "actorSpecification": 1,
410 "name": "book-library-scraper",
411 "title": "Book Library Scraper",
412 "version": "1.0.0",
413 "storages": {
414 "dataset": "./dataset_schema.json"
415 }
416}
417```
418
419Then create the dataset schema in `.actor/dataset_schema.json`:
420
421```json
422{
423 "actorSpecification": 1,
424 "fields": {},
425 "views": {
426 "overview": {
427 "title": "Overview",
428 "transformation": {
429 "fields": [
430 "pictureUrl",
431 "linkUrl",
432 "textField",
433 "booleanField",
434 "arrayField",
435 "objectField",
436 "dateField",
437 "numericField"
438 ]
439 },
440 "display": {
441 "component": "table",
442 "properties": {
443 "pictureUrl": {
444 "label": "Image",
445 "format": "image"
446 },
447 "linkUrl": {
448 "label": "Link",
449 "format": "link"
450 },
451 "textField": {
452 "label": "Text",
453 "format": "text"
454 },
455 "booleanField": {
456 "label": "Boolean",
457 "format": "boolean"
458 },
459 "arrayField": {
460 "label": "Array",
461 "format": "array"
462 },
463 "objectField": {
464 "label": "Object",
465 "format": "object"
466 },
467 "dateField": {
468 "label": "Date",
469 "format": "date"
470 },
471 "numericField": {
472 "label": "Number",
473 "format": "number"
474 }
475 }
476 }
477 }
478 }
479}
480```
481
482### Structure
483
484```json
485{
486 "actorSpecification": 1,
487 "fields": {},
488 "views": {
489 "<VIEW_NAME>": {
490 "title": "string (required)",
491 "description": "string (optional)",
492 "transformation": {
493 "fields": ["string (required)"],
494 "unwind": ["string (optional)"],
495 "flatten": ["string (optional)"],
496 "omit": ["string (optional)"],
497 "limit": "integer (optional)",
498 "desc": "boolean (optional)"
499 },
500 "display": {
501 "component": "table (required)",
502 "properties": {
503 "<FIELD_NAME>": {
504 "label": "string (optional)",
505 "format": "text|number|date|link|boolean|image|array|object (optional)"
506 }
507 }
508 }
509 }
510 }
511}
512```
513
514**Dataset Schema Properties:**
515
516- `actorSpecification` (integer, required) - Specifies the version of dataset schema structure document (currently only version 1)
517- `fields` (JSONSchema object, required) - Schema of one dataset object (use JsonSchema Draft 2020-12 or compatible)
518- `views` (DatasetView object, required) - Object with API and UI views description
519
520**DatasetView Properties:**
521
522- `title` (string, required) - Visible in UI Output tab and API
523- `description` (string, optional) - Only available in API response
524- `transformation` (ViewTransformation object, required) - Data transformation applied when loading from Dataset API
525- `display` (ViewDisplay object, required) - Output tab UI visualization definition
526
527**ViewTransformation Properties:**
528
529- `fields` (string[], required) - Fields to present in output (order matches column order)
530- `unwind` (string[], optional) - Deconstructs nested children into parent object
531- `flatten` (string[], optional) - Transforms nested object into flat structure
532- `omit` (string[], optional) - Removes specified fields from output
533- `limit` (integer, optional) - Maximum number of results (default: all)
534- `desc` (boolean, optional) - Sort order (true = newest first)
535
536**ViewDisplay Properties:**
537
538- `component` (string, required) - Only `table` is available
539- `properties` (Object, optional) - Keys matching `transformation.fields` with ViewDisplayProperty values
540
541**ViewDisplayProperty Properties:**
542
543- `label` (string, optional) - Table column header
544- `format` (string, optional) - One of: `text`, `number`, `date`, `link`, `boolean`, `image`, `array`, `object`
545
546## Key-Value Store Schema Specification
547
548The key-value store schema organizes keys into logical groups called collections for easier data management.
549
550### Example
551
552Consider an example Actor that calls `Actor.setValue()` to save records into the key-value store:
553
554```javascript
555import { Actor } from 'apify';
556// Initialize the JavaScript SDK
557await Actor.init();
558
559/**
560 * Actor code
561 */
562await Actor.setValue('document-1', 'my text data', { contentType: 'text/plain' });
563
564await Actor.setValue(`image-${imageID}`, imageBuffer, { contentType: 'image/jpeg' });
565
566// Exit successfully
567await Actor.exit();
568```
569
570To configure the key-value store schema, reference a schema file in `.actor/actor.json`:
571
572```json
573{
574 "actorSpecification": 1,
575 "name": "data-collector",
576 "title": "Data Collector",
577 "version": "1.0.0",
578 "storages": {
579 "keyValueStore": "./key_value_store_schema.json"
580 }
581}
582```
583
584Then create the key-value store schema in `.actor/key_value_store_schema.json`:
585
586```json
587{
588 "actorKeyValueStoreSchemaVersion": 1,
589 "title": "Key-Value Store Schema",
590 "collections": {
591 "documents": {
592 "title": "Documents",
593 "description": "Text documents stored by the Actor",
594 "keyPrefix": "document-"
595 },
596 "images": {
597 "title": "Images",
598 "description": "Images stored by the Actor",
599 "keyPrefix": "image-",
600 "contentTypes": ["image/jpeg"]
601 }
602 }
603}
604```
605
606### Structure
607
608```json
609{
610 "actorKeyValueStoreSchemaVersion": 1,
611 "title": "string (required)",
612 "description": "string (optional)",
613 "collections": {
614 "<COLLECTION_NAME>": {
615 "title": "string (required)",
616 "description": "string (optional)",
617 "key": "string (conditional - use key OR keyPrefix)",
618 "keyPrefix": "string (conditional - use key OR keyPrefix)",
619 "contentTypes": ["string (optional)"],
620 "jsonSchema": "object (optional)"
621 }
622 }
623}
624```
625
626**Key-Value Store Schema Properties:**
627
628- `actorKeyValueStoreSchemaVersion` (integer, required) - Version of key-value store schema structure document (currently only version 1)
629- `title` (string, required) - Title of the schema
630- `description` (string, optional) - Description of the schema
631- `collections` (Object, required) - Object where each key is a collection ID and value is a Collection object
632
633**Collection Properties:**
634
635- `title` (string, required) - Collection title shown in UI tabs
636- `description` (string, optional) - Description appearing in UI tooltips
637- `key` (string, conditional) - Single specific key for this collection
638- `keyPrefix` (string, conditional) - Prefix for keys included in this collection
639- `contentTypes` (string[], optional) - Allowed content types for validation
640- `jsonSchema` (object, optional) - JSON Schema Draft 07 format for `application/json` content type validation
641
642Either `key` or `keyPrefix` must be specified for each collection, but not both.
643
644## Actor README
645
646**Always generate a README.md file as part of Actor development.** The README is the Actor's public landing page on Apify Store - it serves as SEO, first impression, documentation, and support page combined.
647
648### Required: Generate README automatically
649
650When building an Actor, always create a `README.md` in the project root. Do not wait for the user to ask for it. The README is a critical part of a complete Actor.
651
652### README structure
653
654Write in Markdown. Use H2 (`##`) for main sections (these become the table of contents) and H3 (`###`) for subsections. Do not use H1 - the Actor name is automatically the H1. Aim for at least 300 words.
655
656Include these sections in order:
657
6581. **What does [Actor name] do?** - 2-3 sentences explaining what it does, what data it extracts, and how to try it. Link to the target website. Mention Apify platform advantages (API access, scheduling, integrations, proxy rotation, monitoring).
6592. **Why use [Actor name]?** - Business use cases and benefits.
6603. **How to use [Actor name]** - Numbered step-by-step tutorial. Keep it simple and reassuring.
6614. **Input** - Describe input fields. Reference the Input tab. Optionally include a screenshot or JSON example of the input schema.
6625. **Output** - Show a simplified JSON output example. Mention "You can download the dataset in various formats such as JSON, HTML, CSV, or Excel."
6636. **Data table** - If the Actor extracts data, include a table of the main data fields it outputs.
6647. **Pricing / Cost estimation** - Set expectations on cost. Mention free tier limits if applicable. Frame as "How much does it cost to scrape [target site]?"
6658. **Tips or Advanced options** - How to optimize runs, limit compute units, improve speed or accuracy.
6669. **FAQ, disclaimers, and support** - Legality disclaimer for scrapers, known limitations, link to Issues tab for feedback, mention custom solution availability.
667
668### README best practices
669
670- Write SEO-friendly headings with relevant keywords (e.g., "How to scrape [site] data" not just "Tutorial")
671- Bold the most important words in the intro
672- The first 25% of the README matters most - front-load the value proposition
673- Match the tone to the target audience: simple language for no-code users, technical details for developers
674- Include a JSON output example showing 1-2 representative items
675- Reference these top Actors for README best practices: https://apify.com/apify/instagram-scraper and https://apify.com/compass/crawler-google-places
676- Embed YouTube video URLs on their own line (Apify Console auto-renders them)
677- Use HTML for image sizing if needed; CSS is not supported
678
679## MCP Tools
680
681### Apify MCP
682
683If the Apify MCP server is configured, use these tools for documentation:
684
685- `search-apify-docs` - Search documentation
686- `fetch-apify-docs` - Get full doc pages
687
688Otherwise, reference: `@https://mcp.apify.com/`
689
690### Playwright MCP (debugging)
691
692The Playwright MCP server is a useful tool for debugging Actors that interact with the web - it lets the agent drive a real browser to inspect pages, capture selectors, and reproduce issues.
693
694Install with the Claude Code CLI:
695
696```bash
697claude mcp add playwright npx @playwright/mcp@latest
698```
699
700Or add it manually to your MCP config:
701
702```json
703{
704 "mcpServers": {
705 "playwright": {
706 "command": "npx",
707 "args": ["@playwright/mcp@latest"]
708 }
709 }
710}
711```
712
713## Resources
714
715- [docs.apify.com/llms.txt](https://docs.apify.com/llms.txt) - Quick reference
716- [docs.apify.com/llms-full.txt](https://docs.apify.com/llms-full.txt) - Complete docs
717- [crawlee.dev](https://crawlee.dev) - Crawlee documentation
718- [whitepaper.actor](https://raw.githubusercontent.com/apify/actor-whitepaper/refs/heads/master/README.md) - Complete Actor specification