diff --git a/components/apify/README.md b/components/apify/README.md index a34f3990bac5f..1cde1a46a5e99 100644 --- a/components/apify/README.md +++ b/components/apify/README.md @@ -14,4 +14,4 @@ The Apify API unleashes the power to automate web scraping, process data, and or **Real Estate Listings Aggregator** -- Create a workflow where Apify actors periodically fetch the latest real estate listings from multiple websites. Process and normalize the data with Pipedream's code steps, then automatically update a Google Sheet that serves as a central repository for all listings, keeping potential buyers informed with the latest options. +- Create a workflow where Apify Actors periodically fetch the latest real estate listings from multiple websites. Process and normalize the data with Pipedream's code steps, then automatically update a Google Sheet that serves as a central repository for all listings, keeping potential buyers informed with the latest options. diff --git a/components/apify/actions/run-actor/run-actor.mjs b/components/apify/actions/run-actor/run-actor.mjs index e3d4f1aa24dc5..75ca7d4933f3f 100644 --- a/components/apify/actions/run-actor/run-actor.mjs +++ b/components/apify/actions/run-actor/run-actor.mjs @@ -26,7 +26,7 @@ export default { actorSource: { type: "string", label: "Search Actors from", - description: "Where to search for Actors. Valid options are Store and Recently used Actors.", + description: "Where to search for Actors. Choose **Apify Store Actors** to browse the public [Apify Store](https://apify.com/store), or **Recently used Actors** to pick from Actors you've run before.", options: [ { label: "Apify Store Actors", @@ -61,10 +61,11 @@ export default { reloadProps: true, optional: true, }, - runAsynchronously: { + // Renamed from `runAsynchronously` for clarity. + waitForFinish: { type: "boolean", - label: "Run Asynchronously", - description: "Set to `true` to run the Actor asynchronously", + label: "Wait for Finish", + description: "If `true` (default), the step waits for the Actor run to finish and returns its output. If `false`, the step starts the run and returns immediately with the run details, without waiting.", reloadProps: true, default: true, }, @@ -162,7 +163,7 @@ export default { async getBuildOrThrow(actorId, buildTag) { const build = await this.apify.getBuild(actorId, buildTag); if (!build) { - throw new Error(`No build found for actor ${actorId}`); + throw new Error(`No build found for Actor ${actorId}`); } return build; }, @@ -180,14 +181,14 @@ export default { : build.inputSchema; } catch (err) { throw new Error( - `Failed to parse inputSchema for actor ${actorId}: ${err.message}`, + `Failed to parse inputSchema for Actor ${actorId}: ${err.message}`, ); } } // Case 3: no schema at all (e.g. apify/hello-world) const noSchemaError = new Error( - `No input schema found for actor ${actorId}. Has it been built successfully?`, + `No input schema found for Actor ${actorId}. Has it been built successfully?`, ); noSchemaError.noInputSchema = true; throw noSchemaError; @@ -273,6 +274,24 @@ export default { }, async additionalProps() { const props = {}; + + // Show a hint if user set actorId to sentinel value "". + // Displayed only when user has no recently-used Actors. + if (this.actorId === "") { + return { + actorHint: { + type: "alert", + alertType: "info", + content: "No Actor selected. Set **Search Actors from** to **Apify Store Actors** above, then choose an Actor to run.", + }, + }; + } + + // Not picked yet (undefined): show nothing rather than erroring. + if (!this.actorId) { + return props; + } + let memoryLimits = getMemoryLimits(); try { const build = await this.getBuildOrThrow(this.actorId, this.buildTag); @@ -345,7 +364,7 @@ export default { // Actor memory dropdown, filtered by per-actor limits. props.memory = buildMemoryProp(memoryLimits); - if (!this.runAsynchronously) { + if (this.waitForFinish) { props.outputRecordKey = { type: "string", label: "Output Record Key", @@ -372,7 +391,7 @@ export default { apify, actorId, buildTag, - runAsynchronously, + waitForFinish, outputRecordKey, timeout, memory, @@ -395,7 +414,7 @@ export default { if (!actorDetails.stats?.totalBuilds || actorDetails.stats.totalBuilds === 0) { throw new Error( - `Actor "${actorDetails.title || actorDetails.name}" has no builds. Please build it first before running.`, + `Actor "${actorDetails.title || actorDetails.name}" has no builds yet and can't be run until it's built. Open the Actor in Apify Console and build it (Source → Code → Build), or trigger a build via the Apify CLI/API, then run this step again.`, ); } @@ -460,8 +479,8 @@ export default { let run; - if (runAsynchronously) { - // async run + if (!waitForFinish) { + // async run — start and return immediately without waiting run = await apify.runActorAsynchronously({ actorId, data: input, diff --git a/components/apify/apify.app.mjs b/components/apify/apify.app.mjs index 3b9a8b6a88eac..4f996e7d25771 100644 --- a/components/apify/apify.app.mjs +++ b/components/apify/apify.app.mjs @@ -29,7 +29,7 @@ export default { actorId: { type: "string", label: "Actor", - description: "Select the Actor, enter the Actor ID, or use a tilde-separated combination of the owner's username and the Actor name.", + description: "Select an Actor from the dropdown, enter an Actor ID, or use a tilde-separated owner/name identifier (e.g. `apify~web-scraper`). If the list is empty, switch \"Search Actors from\" to **Apify Store Actors**.", async options({ page, actorSource, }) { @@ -168,6 +168,15 @@ export default { limit: LIMIT, }); + if (page === 0 && items.length === 0 && actorSource !== "store") { + return [ + { + label: "No recent Actors, switch to \"Apify Store Actors\" above", + value: "", + }, + ]; + } + return items.map((actor) => ({ label: this.formatActorOrTaskLabel(actor), value: actor.id, diff --git a/components/apify/sources/common/base.mjs b/components/apify/sources/common/base.mjs index 49fa7ae5621bf..a7530432fe678 100644 --- a/components/apify/sources/common/base.mjs +++ b/components/apify/sources/common/base.mjs @@ -2,6 +2,7 @@ import { createHash } from "crypto"; import { WEBHOOK_EVENT_TYPE_GROUPS, WEBHOOK_EVENT_TYPES, } from "@apify/consts"; +import { ConfigurationError } from "@pipedream/platform"; import apify from "../../apify.app.mjs"; export default { @@ -40,6 +41,14 @@ export default { }, hooks: { async activate() { + // Block empty or unset condition targets. + if (Object.values(this.getCondition()).some((value) => !value)) { + throw new ConfigurationError( + this.getEmptyConditionMessage?.() + ?? "No target selected. Select an Actor or Task before deploying.", + ); + } + // Whitelist terminal states only; fall back to all when none are valid. const selected = (this.eventTypes ?? []) .filter((type) => WEBHOOK_EVENT_TYPE_GROUPS.ACTOR_RUN_TERMINAL.includes(type)); diff --git a/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs b/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs index 0dd22d511ea22..8aa58ff5613eb 100644 --- a/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs +++ b/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs @@ -17,7 +17,7 @@ export default { actorSource: { type: "string", label: "Search Actors from", - description: "Where to search for Actors. Valid options are Store and Recently used Actors.", + description: "Where to search for Actors. Choose **Apify Store Actors** to browse the public [Apify Store](https://apify.com/store), or **Recently used Actors** to pick from Actors you've run before.", options: [ { label: "Apify Store Actors", @@ -56,6 +56,9 @@ export default { actorId: this.actorId, }; }, + getEmptyConditionMessage() { + return "No Actor selected. If the list was empty, set \"Search Actors from\" to \"Apify Store Actors\" and pick an Actor before deploying."; + }, getSummary(body) { return `A new Actor run ${body.eventData.actorRunId} has finished`; },