diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index c848e36eb6..61c0267375 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1,47 +1,5 @@ import type { SVGProps } from 'react' -export function UsersIcon(props: SVGProps) { - return ( - - - - - - - ) -} - -export function SettingsIcon(props: SVGProps) { - return ( - - - - - ) -} - export function SearchIcon(props: SVGProps) { return ( ) { strokeLinejoin='round' > - - - ) -} - -export function Spinner() { - return ( -
- - - - -
- ) -} - -export function AgentIcon(props: SVGProps) { - return ( - - - - - - ) -} - -export function ApiIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ConditionalIcon(props: SVGProps) { - return ( - - - - ) -} - -export function NoteIcon(props: SVGProps) { - return ( - - - - - - - ) -} - -export function AirplaneIcon(props: SVGProps) { - return ( - - - - ) -} - -export function WorkIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function WorkflowIcon(props: SVGProps) { - return ( - - - - - - - - ) -} - -export function WarnIcon(props: SVGProps) { - return ( - - - - ) -} - -export function UploadIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function TrashIcon(props: SVGProps) { - return ( - - - - ) -} - -export function StudentIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function SignalIcon(props: SVGProps) { - return ( - - - - ) -} - -export function SectionIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ReminderIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function DatabaseIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function CrateIcon(props: SVGProps) { - return ( - - - - ) -} - -export function CookieIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ErrorIcon(props: SVGProps) { - return ( - - + ) } -export function ChromeIcon(props: SVGProps) { +export function AgentIcon(props: SVGProps) { return ( - - ) -} - -export function CalendarIcon(props: SVGProps) { - return ( - @@ -616,7 +55,7 @@ export function CalendarIcon(props: SVGProps) { ) } -export function MessagesIcon(props: SVGProps) { +export function ApiIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > ) { ) } -export function NotificationsIcon(props: SVGProps) { +export function ConditionalIcon(props: SVGProps) { return ( ) } -export function MailIcon(props: SVGProps) { +export function NoteIcon(props: SVGProps) { return ( - + + + ) } -export function CodeIcon(props: SVGProps) { +export function WorkflowIcon(props: SVGProps) { return ( - - - ) -} - -export function ChartBarIcon(props: SVGProps) { - return ( - - - - ) -} - -export function AtomIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function ElevatorIcon(props: SVGProps) { - return ( - - - ) -} - -export function DollarIcon(props: SVGProps) { - return ( - - @@ -788,18 +190,18 @@ export function DollarIcon(props: SVGProps) { ) } -export function CreditCardIcon(props: SVGProps) { +export function SignalIcon(props: SVGProps) { return ( ) { ) } -export function BoatIcon(props: SVGProps) { +export function CalendarIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > ) { ) } -export function CancelIcon(props: SVGProps) { +export function MessagesIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > - ) { ) } -export function BankIcon(props: SVGProps) { +export function NotificationsIcon(props: SVGProps) { return ( ) { ) } -export function AmbulanceIcon(props: SVGProps) { +export function MailIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > - - ) -} - -export function ComponentIcon(props: SVGProps) { - return ( - - ) { ) } -export function BrightIcon(props: SVGProps) { +export function CodeIcon(props: SVGProps) { return ( @@ -942,21 +316,19 @@ export function BrightIcon(props: SVGProps) { ) } -export function CrewAIIcon(props: SVGProps) { +export function ChartBarIcon(props: SVGProps) { return ( ) @@ -4428,3 +3800,36 @@ export function SmtpIcon(props: SVGProps) { ) } + +export function ApifyIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 3ace0e3907..5a31d3c47d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -5,6 +5,7 @@ import type { ComponentType, SVGProps } from 'react' import { AirtableIcon, + ApifyIcon, ApolloIcon, ArxivIcon, AsanaIcon, @@ -187,5 +188,6 @@ export const blockTypeToIconMap: Record = { asana: AsanaIcon, arxiv: ArxivIcon, apollo: ApolloIcon, + apify: ApifyIcon, airtable: AirtableIcon, } diff --git a/apps/docs/content/docs/en/tools/apify.mdx b/apps/docs/content/docs/en/tools/apify.mdx new file mode 100644 index 0000000000..d26a1eb70a --- /dev/null +++ b/apps/docs/content/docs/en/tools/apify.mdx @@ -0,0 +1,94 @@ +--- +title: Apify +description: Run Apify actors and retrieve results +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Apify](https://apify.com/) is a powerful platform for building, deploying, and running web automation and web scraping actors at scale. Apify enables you to extract useful data from any website, automate workflows, and connect your data pipelines seamlessly. + +With Apify, you can: + +- **Run ready-made or custom actors**: Integrate public actors or develop your own, automating a wide range of web data extraction and browser tasks. +- **Retrieve datasets**: Access and manage structured datasets collected by actors in real time. +- **Scale web automation**: Leverage cloud infrastructure to run tasks reliably, asynchronously or synchronously, with robust error handling. + +In Sim, the Apify integration allows your agents to perform core Apify operations programmatically: + +- **Run Actor (Sync)**: Use `apify_run_actor_sync` to launch an Apify actor and wait for its completion, retrieving the results as soon as the run finishes. +- **Run Actor (Async)**: Use `apify_run_actor_async` to start an actor in the background and periodically poll for results, suitable for longer or complex jobs. + +These operations equip your agents to automate, scrape, and orchestrate data collection or browser automation tasks directly inside workflows — all with flexible configuration and result handling, without the need for manual runs or external tools. Integrate Apify as a dynamic automation and data-extraction engine that programmatically powers your agents' web-scale workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Apify into your workflow. Run any Apify actor with custom input and retrieve results. Supports both synchronous and asynchronous execution with automatic dataset fetching. + + + +## Tools + +### `apify_run_actor_sync` + +Run an APIFY actor synchronously and get results (max 5 minutes) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | +| `actorId` | string | Yes | Actor ID or username/actor-name \(e.g., "janedoe/my-actor" or actor ID\) | +| `input` | string | No | Actor input as JSON string. See actor documentation for required fields. | +| `timeout` | number | No | Timeout in seconds \(default: actor default\) | +| `build` | string | No | Actor build to run \(e.g., "latest", "beta", or build tag/number\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the actor run succeeded | +| `runId` | string | APIFY run ID | +| `status` | string | Run status \(SUCCEEDED, FAILED, etc.\) | +| `datasetId` | string | Dataset ID containing results | +| `items` | array | Dataset items \(if completed\) | + +### `apify_run_actor_async` + +Run an APIFY actor asynchronously with polling for long-running tasks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | +| `actorId` | string | Yes | Actor ID or username/actor-name \(e.g., "janedoe/my-actor" or actor ID\) | +| `input` | string | No | Actor input as JSON string | +| `waitForFinish` | number | No | Initial wait time in seconds \(0-60\) before polling starts | +| `itemLimit` | number | No | Max dataset items to fetch \(1-250000, default 100\) | +| `timeout` | number | No | Timeout in seconds \(default: actor default\) | +| `build` | string | No | Actor build to run \(e.g., "latest", "beta", or build tag/number\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the actor run succeeded | +| `runId` | string | APIFY run ID | +| `status` | string | Run status \(SUCCEEDED, FAILED, etc.\) | +| `datasetId` | string | Dataset ID containing results | +| `items` | array | Dataset items \(if completed\) | + + + +## Notes + +- Category: `tools` +- Type: `apify` diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index e8a4b7478a..a49297bad1 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -2,6 +2,7 @@ "pages": [ "index", "airtable", + "apify", "apollo", "arxiv", "asana", diff --git a/apps/sim/blocks/blocks/apify.ts b/apps/sim/blocks/blocks/apify.ts new file mode 100644 index 0000000000..e619be80e8 --- /dev/null +++ b/apps/sim/blocks/blocks/apify.ts @@ -0,0 +1,142 @@ +import { ApifyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { RunActorResult } from '@/tools/apify/types' + +export const ApifyBlock: BlockConfig = { + type: 'apify', + name: 'Apify', + description: 'Run Apify actors and retrieve results', + longDescription: + 'Integrate Apify into your workflow. Run any Apify actor with custom input and retrieve results. Supports both synchronous and asynchronous execution with automatic dataset fetching.', + docsLink: 'https://docs.sim.ai/tools/apify', + category: 'tools', + bgColor: '#E0E0E0', + icon: ApifyIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Run Actor', id: 'apify_run_actor_sync' }, + { label: 'Run Actor (Async)', id: 'apify_run_actor_async' }, + ], + value: () => 'apify_run_actor_sync', + }, + { + id: 'apiKey', + title: 'Apify API Token', + type: 'short-input', + password: true, + placeholder: 'Enter your Apify API token', + required: true, + }, + { + id: 'actorId', + title: 'Actor ID', + type: 'short-input', + placeholder: 'e.g., janedoe/my-actor or actor ID', + required: true, + }, + { + id: 'input', + title: 'Actor Input', + type: 'code', + language: 'json', + placeholder: '{\n "startUrl": "https://example.com",\n "maxPages": 10\n}', + required: false, + }, + { + id: 'timeout', + title: 'Timeout', + type: 'short-input', + placeholder: 'Actor timeout in seconds', + required: false, + }, + { + id: 'build', + title: 'Build', + type: 'short-input', + placeholder: 'Actor build (e.g., "latest", "beta", or build tag)', + required: false, + }, + { + id: 'waitForFinish', + title: 'Wait For Finish', + type: 'short-input', + placeholder: 'Initial wait time in seconds (0-60)', + required: false, + condition: { + field: 'operation', + value: 'apify_run_actor_async', + }, + }, + { + id: 'itemLimit', + title: 'Item Limit', + type: 'short-input', + placeholder: 'Max dataset items to fetch (1-250000)', + required: false, + condition: { + field: 'operation', + value: 'apify_run_actor_async', + }, + }, + ], + + tools: { + access: ['apify_run_actor_sync', 'apify_run_actor_async'], + config: { + tool: (params) => params.operation, + params: (params: Record) => { + const { operation, ...rest } = params + const result: Record = { + apiKey: rest.apiKey, + actorId: rest.actorId, + } + + if (rest.input) { + result.input = rest.input + } + + if (rest.timeout) { + result.timeout = Number(rest.timeout) + } + + if (rest.build) { + result.build = rest.build + } + + if (rest.waitForFinish) { + result.waitForFinish = Number(rest.waitForFinish) + } + + if (rest.itemLimit) { + result.itemLimit = Number(rest.itemLimit) + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Apify API token' }, + actorId: { type: 'string', description: 'Actor ID or username/actor-name' }, + input: { type: 'string', description: 'Actor input as JSON string' }, + timeout: { type: 'number', description: 'Timeout in seconds' }, + build: { type: 'string', description: 'Actor build version' }, + waitForFinish: { type: 'number', description: 'Initial wait time in seconds' }, + itemLimit: { type: 'number', description: 'Max dataset items to fetch' }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the actor run succeeded' }, + runId: { type: 'string', description: 'Apify run ID' }, + status: { type: 'string', description: 'Run status (SUCCEEDED, FAILED, etc.)' }, + datasetId: { type: 'string', description: 'Dataset ID containing results' }, + items: { type: 'json', description: 'Dataset items (if completed)' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index e7da0a7e57..994f3402cd 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -2,6 +2,7 @@ import { AgentBlock } from '@/blocks/blocks/agent' import { AirtableBlock } from '@/blocks/blocks/airtable' import { ApiBlock } from '@/blocks/blocks/api' import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger' +import { ApifyBlock } from '@/blocks/blocks/apify' import { ApolloBlock } from '@/blocks/blocks/apollo' import { ArxivBlock } from '@/blocks/blocks/arxiv' import { AsanaBlock } from '@/blocks/blocks/asana' @@ -119,6 +120,7 @@ import type { BlockConfig } from '@/blocks/types' export const registry: Record = { agent: AgentBlock, airtable: AirtableBlock, + apify: ApifyBlock, apollo: ApolloBlock, api: ApiBlock, arxiv: ArxivBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c848e36eb6..61c0267375 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1,47 +1,5 @@ import type { SVGProps } from 'react' -export function UsersIcon(props: SVGProps) { - return ( - - - - - - - ) -} - -export function SettingsIcon(props: SVGProps) { - return ( - - - - - ) -} - export function SearchIcon(props: SVGProps) { return ( ) { strokeLinejoin='round' > - - - ) -} - -export function Spinner() { - return ( -
- - - - -
- ) -} - -export function AgentIcon(props: SVGProps) { - return ( - - - - - - ) -} - -export function ApiIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ConditionalIcon(props: SVGProps) { - return ( - - - - ) -} - -export function NoteIcon(props: SVGProps) { - return ( - - - - - - - ) -} - -export function AirplaneIcon(props: SVGProps) { - return ( - - - - ) -} - -export function WorkIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function WorkflowIcon(props: SVGProps) { - return ( - - - - - - - - ) -} - -export function WarnIcon(props: SVGProps) { - return ( - - - - ) -} - -export function UploadIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function TrashIcon(props: SVGProps) { - return ( - - - - ) -} - -export function StudentIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function SignalIcon(props: SVGProps) { - return ( - - - - ) -} - -export function SectionIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ReminderIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function DatabaseIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function CrateIcon(props: SVGProps) { - return ( - - - - ) -} - -export function CookieIcon(props: SVGProps) { - return ( - - - - ) -} - -export function ErrorIcon(props: SVGProps) { - return ( - - + ) } -export function ChromeIcon(props: SVGProps) { +export function AgentIcon(props: SVGProps) { return ( - - ) -} - -export function CalendarIcon(props: SVGProps) { - return ( - @@ -616,7 +55,7 @@ export function CalendarIcon(props: SVGProps) { ) } -export function MessagesIcon(props: SVGProps) { +export function ApiIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > ) { ) } -export function NotificationsIcon(props: SVGProps) { +export function ConditionalIcon(props: SVGProps) { return ( ) } -export function MailIcon(props: SVGProps) { +export function NoteIcon(props: SVGProps) { return ( - + + + ) } -export function CodeIcon(props: SVGProps) { +export function WorkflowIcon(props: SVGProps) { return ( - - - ) -} - -export function ChartBarIcon(props: SVGProps) { - return ( - - - - ) -} - -export function AtomIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function ElevatorIcon(props: SVGProps) { - return ( - - - ) -} - -export function DollarIcon(props: SVGProps) { - return ( - - @@ -788,18 +190,18 @@ export function DollarIcon(props: SVGProps) { ) } -export function CreditCardIcon(props: SVGProps) { +export function SignalIcon(props: SVGProps) { return ( ) { ) } -export function BoatIcon(props: SVGProps) { +export function CalendarIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > ) { ) } -export function CancelIcon(props: SVGProps) { +export function MessagesIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > - ) { ) } -export function BankIcon(props: SVGProps) { +export function NotificationsIcon(props: SVGProps) { return ( ) { ) } -export function AmbulanceIcon(props: SVGProps) { +export function MailIcon(props: SVGProps) { return ( ) { xmlns='http://www.w3.org/2000/svg' > - - ) -} - -export function ComponentIcon(props: SVGProps) { - return ( - - ) { ) } -export function BrightIcon(props: SVGProps) { +export function CodeIcon(props: SVGProps) { return ( @@ -942,21 +316,19 @@ export function BrightIcon(props: SVGProps) { ) } -export function CrewAIIcon(props: SVGProps) { +export function ChartBarIcon(props: SVGProps) { return ( ) @@ -4428,3 +3800,36 @@ export function SmtpIcon(props: SVGProps) { ) } + +export function ApifyIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + ) +} diff --git a/apps/sim/tools/apify/index.ts b/apps/sim/tools/apify/index.ts new file mode 100644 index 0000000000..673855f12f --- /dev/null +++ b/apps/sim/tools/apify/index.ts @@ -0,0 +1,3 @@ +export { apifyRunActorAsyncTool } from './run_actor_async' +export { apifyRunActorSyncTool } from './run_actor_sync' +export type { RunActorParams, RunActorResult } from './types' diff --git a/apps/sim/tools/apify/run_actor_async.ts b/apps/sim/tools/apify/run_actor_async.ts new file mode 100644 index 0000000000..1c613dcde6 --- /dev/null +++ b/apps/sim/tools/apify/run_actor_async.ts @@ -0,0 +1,213 @@ +import type { ToolConfig } from '@/tools/types' +import type { RunActorParams, RunActorResult } from './types' + +const POLL_INTERVAL_MS = 5000 // 5 seconds between polls +const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time + +export const apifyRunActorAsyncTool: ToolConfig = { + id: 'apify_run_actor_async', + name: 'APIFY Run Actor (Async)', + description: 'Run an APIFY actor asynchronously with polling for long-running tasks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'APIFY API token from console.apify.com/account#/integrations', + }, + actorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Actor ID or username/actor-name (e.g., "janedoe/my-actor" or actor ID)', + }, + input: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Actor input as JSON string', + }, + waitForFinish: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Initial wait time in seconds (0-60) before polling starts', + }, + itemLimit: { + type: 'number', + required: false, + default: 100, + visibility: 'user-or-llm', + description: 'Max dataset items to fetch (1-250000, default 100)', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timeout in seconds (default: actor default)', + }, + build: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Actor build to run (e.g., "latest", "beta", or build tag/number)', + }, + }, + + request: { + url: (params) => { + const encodedActorId = encodeURIComponent(params.actorId) + const baseUrl = `https://api.apify.com/v2/acts/${encodedActorId}/runs` + const queryParams = new URLSearchParams() + + queryParams.set('token', params.apiKey) + + if (params.waitForFinish !== undefined) { + const waitTime = Math.max(0, Math.min(params.waitForFinish, 60)) + queryParams.set('waitForFinish', waitTime.toString()) + } + if (params.timeout) { + queryParams.set('timeout', params.timeout.toString()) + } + if (params.build) { + queryParams.set('build', params.build) + } + + return `${baseUrl}?${queryParams.toString()}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let inputData = {} + if (params.input) { + try { + inputData = JSON.parse(params.input) + } catch (e) { + throw new Error('Invalid JSON in input parameter') + } + } + return inputData + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + output: { success: false, runId: '', status: 'ERROR' }, + error: `APIFY API error: ${errorText}`, + } + } + + const data = await response.json() + return { + success: true, + output: data.data, + } + }, + + postProcess: async (result, params) => { + if (!result.success) { + return result + } + + const runData = result.output as any + const runId = runData.id + + let elapsedTime = 0 + + while (elapsedTime < MAX_POLL_TIME_MS) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + elapsedTime += POLL_INTERVAL_MS + + const encodedActorId = encodeURIComponent(params.actorId) + const statusResponse = await fetch( + `https://api.apify.com/v2/acts/${encodedActorId}/runs/${runId}?token=${params.apiKey}`, + { + headers: { + Authorization: `Bearer ${params.apiKey}`, + }, + } + ) + + if (!statusResponse.ok) { + return { + success: false, + output: { success: false, runId, status: 'UNKNOWN' }, + error: 'Failed to fetch run status', + } + } + + const statusData = await statusResponse.json() + const run = statusData.data + + if ( + run.status === 'SUCCEEDED' || + run.status === 'FAILED' || + run.status === 'ABORTED' || + run.status === 'TIMED-OUT' + ) { + if (run.status === 'SUCCEEDED') { + const limit = Math.max(1, Math.min(params.itemLimit || 100, 250000)) + const itemsResponse = await fetch( + `https://api.apify.com/v2/datasets/${run.defaultDatasetId}/items?token=${params.apiKey}&limit=${limit}`, + { + headers: { + Authorization: `Bearer ${params.apiKey}`, + }, + } + ) + + if (itemsResponse.ok) { + const items = await itemsResponse.json() + return { + success: true, + output: { + success: true, + runId, + status: run.status, + datasetId: run.defaultDatasetId, + items, + }, + } + } + } + + return { + success: run.status === 'SUCCEEDED', + output: { + success: run.status === 'SUCCEEDED', + runId, + status: run.status, + datasetId: run.defaultDatasetId, + }, + error: run.status !== 'SUCCEEDED' ? `Actor run ${run.status}` : undefined, + } + } + } + + return { + success: false, + output: { + success: false, + runId, + status: 'TIMEOUT', + }, + error: 'Actor run timed out after 5 minutes of polling', + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the actor run succeeded' }, + runId: { type: 'string', description: 'APIFY run ID' }, + status: { type: 'string', description: 'Run status (SUCCEEDED, FAILED, etc.)' }, + datasetId: { type: 'string', description: 'Dataset ID containing results' }, + items: { type: 'array', description: 'Dataset items (if completed)' }, + }, +} diff --git a/apps/sim/tools/apify/run_actor_sync.ts b/apps/sim/tools/apify/run_actor_sync.ts new file mode 100644 index 0000000000..8ef5533a04 --- /dev/null +++ b/apps/sim/tools/apify/run_actor_sync.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' +import type { RunActorParams, RunActorResult } from './types' + +export const apifyRunActorSyncTool: ToolConfig = { + id: 'apify_run_actor_sync', + name: 'APIFY Run Actor (Sync)', + description: 'Run an APIFY actor synchronously and get results (max 5 minutes)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'APIFY API token from console.apify.com/account#/integrations', + }, + actorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Actor ID or username/actor-name (e.g., "janedoe/my-actor" or actor ID)', + }, + input: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Actor input as JSON string. See actor documentation for required fields.', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timeout in seconds (default: actor default)', + }, + build: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Actor build to run (e.g., "latest", "beta", or build tag/number)', + }, + }, + + request: { + url: (params) => { + const encodedActorId = encodeURIComponent(params.actorId) + const baseUrl = `https://api.apify.com/v2/acts/${encodedActorId}/run-sync-get-dataset-items` + const queryParams = new URLSearchParams() + + queryParams.set('token', params.apiKey) + + if (params.timeout) { + queryParams.set('timeout', params.timeout.toString()) + } + if (params.build) { + queryParams.set('build', params.build) + } + + return `${baseUrl}?${queryParams.toString()}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let inputData = {} + if (params.input) { + try { + inputData = JSON.parse(params.input) + } catch (e) { + throw new Error('Invalid JSON in input parameter') + } + } + return inputData + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + output: { success: false, runId: '', status: 'ERROR', items: [] }, + error: `APIFY API error: ${errorText}`, + } + } + + const items = await response.json() + return { + success: true, + output: { + success: true, + runId: 'sync-execution', + status: 'SUCCEEDED', + items, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the actor run succeeded' }, + runId: { type: 'string', description: 'APIFY run ID' }, + status: { type: 'string', description: 'Run status (SUCCEEDED, FAILED, etc.)' }, + datasetId: { type: 'string', description: 'Dataset ID containing results' }, + items: { type: 'array', description: 'Dataset items (if completed)' }, + }, +} diff --git a/apps/sim/tools/apify/types.ts b/apps/sim/tools/apify/types.ts new file mode 100644 index 0000000000..4face19c80 --- /dev/null +++ b/apps/sim/tools/apify/types.ts @@ -0,0 +1,54 @@ +import type { ToolResponse } from '@/tools/types' + +export interface ApifyActor { + id: string + name: string + username: string + description?: string + stats?: { + lastRunStartedAt?: string + } +} + +export interface RunActorParams { + apiKey: string + actorId: string + input?: string + waitForFinish?: number // For async tool: 0-60 seconds initial wait + itemLimit?: number // For async tool: 1-250000 items, default 100 + timeout?: number + build?: string // Actor build to run (e.g., "latest", "beta", build tag/number) +} + +export interface ApifyRun { + id: string + actId: string + status: + | 'READY' + | 'RUNNING' + | 'SUCCEEDED' + | 'FAILED' + | 'ABORTED' + | 'TIMED-OUT' + | 'ABORTING' + | 'TIMING-OUT' + startedAt: string + finishedAt?: string + defaultDatasetId: string + defaultKeyValueStoreId: string +} + +export interface RunActorResult extends ToolResponse { + output: { + success: boolean + runId: string + status: string + datasetId?: string + items?: any[] + stats?: { + inputRecords?: number + outputRecords?: number + duration?: number + } + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9d993891b6..2a0c55ab99 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4,6 +4,7 @@ import { airtableListRecordsTool, airtableUpdateRecordTool, } from '@/tools/airtable' +import { apifyRunActorAsyncTool, apifyRunActorSyncTool } from '@/tools/apify' import { apolloAccountBulkCreateTool, apolloAccountBulkUpdateTool, @@ -1404,6 +1405,8 @@ export const tools: Record = { airtable_get_record: airtableGetRecordTool, airtable_list_records: airtableListRecordsTool, airtable_update_record: airtableUpdateRecordTool, + apify_run_actor_sync: apifyRunActorSyncTool, + apify_run_actor_async: apifyRunActorAsyncTool, apollo_people_search: apolloPeopleSearchTool, apollo_people_enrich: apolloPeopleEnrichTool, apollo_people_bulk_enrich: apolloPeopleBulkEnrichTool,