Skill 04 · Apify Integration Development
Subchapter 4.4
references/workflow-automation.mdMarkdown13 KBView on GitHub
Design guide for integrating Apify into a workflow-automation platform (Zapier, n8n, Make, Pipedream, Activepieces, or a similar trigger/action/search host). This file is UX-design focused and platform-agnostic: it describes the capability surface, the resource model, and the user-facing behavior, not the host platform’s internal build mechanics, release pipeline, or secrets. Apply the cross-cutting rules from on top.
SKILL.mdThe host platform’s model is rigid: every Apify capability must be expressed as one of trigger, action (create), or search (read). Decide the mapping before writing any code.
| Host type | Apify capability | Purpose |
|---|---|---|
| Trigger (hook) | Actor / Task run finished | Event-driven: start a workflow when a run reaches a terminal status |
| Trigger (hidden, polling) | List Actors / Tasks | Back dynamic dropdowns - not user-facing steps |
| Action (create) | Run Actor; Run Task; Run Actor + get dataset; Run Task + get dataset; Scrape single URL; Set KV record | Synchronous operations that produce or store data |
| Search (read) | Get last run; Get run; List runs; Get dataset items; Get KV record | Find existing records, optionally branch on them |
Reads of the last run or stored data are searches, because that is the host’s mechanism for “find an existing record.” Run-finished is a webhook trigger. Actor/Task selection is a hidden trigger that feeds dropdowns - reusing one hidden trigger across multiple actions keeps the surface DRY.
The complete set of operations a workflow integration should surface. Treat this as the floor, not a menu to trim without reason.
Triggers
Actions (creates)
Searches (reads)
Hidden selectors (back dropdowns, not user steps)
If the host platform cannot represent every operation, prioritize in this order: run-finished trigger, run Actor + get dataset, get dataset items, scrape single URL, run Task + get dataset, get KV record, set KV record.
Organize the action node’s surface as resource -> operation, mirroring both the host platform’s UX convention and Apify’s domain model. A two-level router (resource, then operation) keeps adding an operation a copy-and-adapt task and co-locates each operation’s parameters and I/O. Users navigate Actor -> “Run actor”, Dataset -> “Get items”, etc. Avoid a flat list of dozens of operations.
IDs (Actor, Task, run, dataset, KV store, record key) should use the host’s resource-locator property type with multiple modes: From list, By URL, By ID. Extract the ID from a pasted Console URL (e.g. https://console.apify.com/actors/<id>/input -> <id>).
Offer two selection sources in one selector:
A toggle with “recompute fields when changed” lets users switch the dropdown source without leaving the node. Choose a pagination limit that balances completeness against load time (the host UI must support searching within dropdown results).
The highest-leverage UX piece. When a user selects an Actor, fetch the Actor’s build and translate its inputSchema into the host platform’s form fields dynamically. Hardcoding fields per Actor is unmaintainable across thousands of Actors; a translator supports any Actor’s input as a host form.
Handle the type/editor mappings that have no direct host equivalent, and degrade gracefully rather than crashing:
| Apify type/editor | Host field |
|---|---|
string + javascript/python editor | code field |
string + textarea | multiline text |
string + datepicker | datetime |
string + select / enum / enumSuggestedValues | choices dropdown |
isSecret: true | password |
array + json/keyValue editor | multiline text with a JSON.stringify default |
array + requestListSources/pseudoUrls/globs/stringList | flat string list (re-expand at run time) |
object + proxy editor | info box telling users to set it in Apify Console (host has no proxy UI) |
object + schemaBased | nested children fields |
sectionCaption | helpText/info box (host has no stackable sections) |
Two gotchas:
default would pre-populate a non-removable first item and duplicate entries - move prefill values to placeholder.Offer a sync/async toggle on run-start actions:
Use the asynchronous REST flow under the hood (POST /runs with waitForFinish=0, then poll). Polling must be bounded - use the run’s own timeoutSecs + a grace buffer, with an absolute ceiling fallback (e.g. 24h). Poll at a fixed interval (e.g. 1s). “Run actor and get dataset” must additionally require status === 'SUCCEEDED' before fetching dataset items.
Every run-start action exposes a Maximum Cost per Run field (maxTotalChargeUsd), min: 0, default null. Send it as a query parameter only when non-null and > 0; 0/empty means no limit. Never let this be an Actor input field.
Bare Actor run objects contain only storage IDs, not results - useless downstream. Enrich every run once, centrally, into a single shape returned by all run-producing and run-finding actions:
OUTPUT from the default key-value store (and any user-selected store keys).datasetItems - up to a sensible cap (e.g. 100 items) from the default dataset.datasetItemsFileUrls - pre-built download URLs for JSON/CSV/XML/XLSX/HTML/RSS exports.detailsPageUrl - a deep link to the run in Apify Console.meta, stats, options, userId, raw output, standby).Downstream steps map fields by name regardless of which action produced the run, because every action yields the same shape. Describe this shape to the host UI with sampled output fields (sample ~10 items, merge their keys).
Estimate the full download size before inlining: fetch one item, multiply by the requested item count with a safety margin (e.g. 1.2x), compare to a payload cap (e.g. 10 MB). If it exceeds the cap, do not attempt the inline download - return a warning item plus the dataset file URLs. When a trigger fetches more items than the cap allows, push a warning pointing users to the “Get Dataset Items” action with run.defaultDatasetId for full results. The host platform has payload/time limits the Apify API does not; always offer a file-download fallback.
Map the host’s file-handling primitives (dehydration / stashing / signed URLs) to Apify’s binary KV records. For “Get KV record”:
Use the host’s hook type backed by Apify webhooks:
actorId or actorTaskId, with eventTypes from the user-selected terminal statuses. The webhook’s requestUrl is the host’s target URL. Make registration idempotent - derive a key from every field that distinguishes one registration from another (resource:id:sortedEvents:requestUrl) so re-activating a workflow does not create duplicate webhooks. Persist the created webhook ID so deactivation can delete it.Offer an event multi-select (SUCCEEDED / FAILED / TIMED-OUT / ABORTED) plus an “any” shortcut that expands to all four. Build the condition: { actorId } or { actorTaskId } from the watched resource. Pass the webhook payload through as workflow data - it already carries the run metadata.
Centralize API error mapping in one place, not per action. Match each Apify error to the host’s error category:
token-not-found / auth errors -> an authentication error the user must fix.full-permission-actor-not-approved -> a non-retryable error with the validated approval URL in the message (approval is a manual Console action; retrying cannot help).Use retryable errors only when retrying can actually succeed. Keep error codes (like EPERM) out of any message field the host replaces, so the real text survives.
For consumer-facing automation platforms, prefer OAuth2 with PKCE over an API-token field. Users authorize through a browser; no raw token typing. Disable auto-refresh only if the host cannot surface a refresh failure gracefully - otherwise leave it on. Validate the token with a GET /v2/users/me test call and populate the connection label with the username/email.
If the host is headless-only, fall back to an API-token credential with the same verify call on a “Verify” button.
Beyond generic “run Actor”, ship a curated Scrape single URL action: a 2-field form (url, outputFormat) wrapping a content scraper with sensible defaults (maxCrawlDepth: 0, maxResults: 1). Validate the URL first (new URL() + protocol + hostname check) with an actionable error - bad input must never start a paid run. Return a lean, single-object output: { ...pageMetadata, [outputFormat]: content } - strip all content variants and re-add only the chosen one. This is ideal for LLM flows (a single object beats a dataset array) and lowers the barrier for non-power users. Point power users to the underlying Actor for advanced options.
maxTotalChargeUsd field (null = unlimited).while (true).x-apify-integration-platform header is sent on every outbound request; x-apify-integration-origin: apify-integration-development-skill included if built from this skill.