├── index.js ├── .npmignore ├── docs ├── auth.png ├── actions.png ├── run-actor.png ├── trigger.png └── workflow.png ├── nodes └── Apify │ ├── Apify.methods.ts │ ├── helpers │ ├── index.ts │ ├── methods.ts │ ├── consts.ts │ └── hooks.ts │ ├── ApifyTrigger.node.json │ ├── resources │ ├── actors │ │ ├── hooks.ts │ │ ├── get-last-run │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── run-actor │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── scrape-single-url │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── properties.ts │ │ │ └── execute.ts │ │ ├── run-actor-and-get-dataset │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── index.ts │ │ └── router.ts │ ├── datasets │ │ ├── hooks.ts │ │ ├── get-items │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── index.ts │ │ └── router.ts │ ├── actor-runs │ │ ├── hooks.ts │ │ ├── get-run │ │ │ ├── hooks.ts │ │ │ ├── properties.ts │ │ │ ├── index.ts │ │ │ └── execute.ts │ │ ├── get-actor-runs │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── get-user-runs-list │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── index.ts │ │ └── router.ts │ ├── actor-tasks │ │ ├── hooks.ts │ │ ├── run-task │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── run-task-and-get-dataset │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── execute.ts │ │ │ └── properties.ts │ │ ├── index.ts │ │ └── router.ts │ ├── key-value-stores │ │ ├── hooks.ts │ │ ├── get-key-value-store-record │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── properties.ts │ │ │ └── execute.ts │ │ ├── index.ts │ │ └── router.ts │ ├── hooks.ts │ ├── router.ts │ ├── index.ts │ ├── keyValueStoreResourceLocator.ts │ ├── keyValueStoreRecordKeyResourceLocator.ts │ ├── runResourceLocator.ts │ ├── actorTaskResourceLocator.ts │ ├── userActorResourceLocator.ts │ ├── executeActor.ts │ ├── actorResourceLocator.ts │ └── genericFunctions.ts │ ├── __tests__ │ ├── utils │ │ ├── getNodeResultData.ts │ │ ├── nodeTypesClass.ts │ │ ├── executeWorkflow.ts │ │ └── credentialHelper.ts │ ├── workflows │ │ ├── actors │ │ │ ├── scrape-single-url.workflow.json │ │ │ ├── get-last-run.workflow.json │ │ │ ├── run-actor.workflow.json │ │ │ ├── run-actor-wait-for-finish.workflow.json │ │ │ └── run-actor-and-get-dataset.workflow.json │ │ ├── datasets │ │ │ └── get-items.workflow.json │ │ ├── actor-runs │ │ │ ├── get-user-runs-list.workflow.json │ │ │ ├── get-run.workflow.json │ │ │ └── get-runs.workflow.json │ │ ├── webhook │ │ │ └── webhook.workflow.json │ │ ├── actor-tasks │ │ │ ├── run-task.workflow.json │ │ │ ├── run-task-wait-for-finish.workflow.json │ │ │ └── run-task-and-get-dataset.workflow.json │ │ └── key-value-stores │ │ │ └── get-key-value-store-record.workflow.json │ ├── ApifyTrigger.node.spec.ts │ └── Apify.node.spec.ts │ ├── Apify.properties.ts │ ├── apify.svg │ ├── Apify.node.json │ ├── Apify.node.ts │ └── ApifyTrigger.node.ts ├── .gitignore ├── .vscode └── extensions.json ├── jest.config.js ├── .editorconfig ├── .eslintrc.prepublish.js ├── gulpfile.js ├── icons └── apify.svg ├── credentials ├── ApifyApi.credentials.ts └── ApifyOAuth2Api.credentials.ts ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── LICENSE.md ├── .prettierrc.js ├── .eslintrc.js ├── package.json ├── README_TEMPLATE.md ├── tslint.json ├── nodes.config.js ├── CODE_OF_CONDUCT.md └── README.md /index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tsbuildinfo 3 | -------------------------------------------------------------------------------- /docs/auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apify/n8n-nodes-apify/HEAD/docs/auth.png -------------------------------------------------------------------------------- /docs/actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apify/n8n-nodes-apify/HEAD/docs/actions.png -------------------------------------------------------------------------------- /docs/run-actor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apify/n8n-nodes-apify/HEAD/docs/run-actor.png -------------------------------------------------------------------------------- /docs/trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apify/n8n-nodes-apify/HEAD/docs/trigger.png -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apify/n8n-nodes-apify/HEAD/docs/workflow.png -------------------------------------------------------------------------------- /nodes/Apify/Apify.methods.ts: -------------------------------------------------------------------------------- 1 | import { methods } from './resources'; 2 | 3 | export { methods }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | yarn.lock 8 | .vscode/launch.json 9 | -------------------------------------------------------------------------------- /nodes/Apify/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * as hooks from './hooks'; 2 | export * as methods from './methods'; 3 | export * as consts from './consts'; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /nodes/Apify/ApifyTrigger.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-apifyTrigger", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Development", "Developer Tools"], 6 | "resources": { 7 | "credentialDocumentation": [], 8 | "primaryDocumentation": [] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/nodes', '/credentials'], 5 | testMatch: ['**/__tests__/**/?(*.)+(spec).ts'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'json'], 10 | }; 11 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | export function runHooks(properties: INodeProperties[]): { 4 | properties: INodeProperties[]; 5 | methods: INodeType['methods']; 6 | } { 7 | return { 8 | properties, 9 | methods: {}, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | export function runHooks(properties: INodeProperties[]): { 4 | properties: INodeProperties[]; 5 | methods: INodeType['methods']; 6 | } { 7 | return { 8 | properties, 9 | methods: {}, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | export function runHooks(properties: INodeProperties[]): { 4 | properties: INodeProperties[]; 5 | methods: INodeType['methods']; 6 | } { 7 | return { 8 | properties, 9 | methods: {}, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | export function runHooks(properties: INodeProperties[]): { 4 | properties: INodeProperties[]; 5 | methods: INodeType['methods']; 6 | } { 7 | return { 8 | properties, 9 | methods: {}, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | export function runHooks(properties: INodeProperties[]): { 4 | properties: INodeProperties[]; 5 | methods: INodeType['methods']; 6 | } { 7 | return { 8 | properties, 9 | methods: {}, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-run/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/get-last-run/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/get-items/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/scrape-single-url/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-actor-runs/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-user-runs-list/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.prepublish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | extends: "./.eslintrc.js", 6 | 7 | overrides: [ 8 | { 9 | files: ['package.json'], 10 | plugins: ['eslint-plugin-n8n-nodes-base'], 11 | rules: { 12 | 'n8n-nodes-base/community-package-json-name-still-default': 'error', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor-and-get-dataset/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task-and-get-dataset/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/get-key-value-store-record/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | export function runHooks( 4 | option: INodePropertyOptions, 5 | properties: INodeProperties[], 6 | ): { 7 | option: INodePropertyOptions; 8 | properties: INodeProperties[]; 9 | } { 10 | return { 11 | option, 12 | properties, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-run/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Run ID', 6 | name: 'runId', 7 | required: true, 8 | default: '', 9 | type: 'string', 10 | displayOptions: { 11 | show: { 12 | resource: ['Actor runs'], 13 | operation: ['Get run'], 14 | }, 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/utils/getNodeResultData.ts: -------------------------------------------------------------------------------- 1 | import { IRun, ITaskData } from 'n8n-workflow'; 2 | 3 | export const getRunTaskDataByNodeName = (result: IRun, nodeName: string) => { 4 | return result.data.resultData.runData[nodeName]; 5 | }; 6 | 7 | export const getTaskData = (result: ITaskData) => { 8 | return result.data?.main[0]?.[0].json; 9 | }; 10 | 11 | export const getTaskArrayData = (result: ITaskData) => { 12 | return result.data?.main[0]; 13 | }; 14 | -------------------------------------------------------------------------------- /nodes/Apify/helpers/methods.ts: -------------------------------------------------------------------------------- 1 | import { INodeType } from 'n8n-workflow'; 2 | 3 | type IMethodModule = INodeType['methods']; 4 | 5 | /** 6 | * Merge all methods from all modules into one object 7 | * @param modules: IMethodModule[] 8 | * @returns methods: INodeType['methods'] 9 | */ 10 | export function aggregateNodeMethods(modules: IMethodModule[]): INodeType['methods'] { 11 | return modules.reduce((methods, module) => { 12 | return { 13 | ...methods, 14 | ...module, 15 | }; 16 | }, {}); 17 | } 18 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { task, src, dest } = require('gulp'); 3 | 4 | task('build:icons', copyIcons); 5 | 6 | function copyIcons() { 7 | const nodeSource = path.resolve('nodes', '**', '*.{png,svg}'); 8 | const nodeDestination = path.resolve('dist', 'nodes'); 9 | 10 | src(nodeSource).pipe(dest(nodeDestination)); 11 | 12 | const credSource = path.resolve('credentials', '**', '*.{png,svg}'); 13 | const credDestination = path.resolve('dist', 'credentials'); 14 | 15 | return src(credSource).pipe(dest(credDestination)); 16 | } 17 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/get-items/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Get items'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Get Items', 10 | value: 'Get items', 11 | action: 'Get dataset items', 12 | description: 'Retrieves items from a dataset', 13 | }; 14 | 15 | const { properties, option } = runHooks(rawOption, rawProperties); 16 | 17 | export { option, properties }; 18 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/get-key-value-store-record/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | const name = 'Get Key-Value Store Record'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: name, 10 | value: name, 11 | action: 'Get key-value store record', 12 | description: 'Gets a value stored in the key-value store under a specific key', 13 | }; 14 | 15 | const { properties, option } = runHooks(rawOption, rawProperties); 16 | 17 | export { option, properties, name }; 18 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-actor-runs/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Get runs'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: name, 10 | value: name, 11 | action: name, 12 | description: 13 | 'Gets a list of Actor runs. This endpoint is useful for retrieving a history of runs, their statuses, and other data.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Run task'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Run Task', 10 | value: 'Run task', 11 | action: 'Run task', 12 | description: 13 | 'Runs an Actor Task and return all associated details. You can optionally override the Actor’s input configuration by providing a custom body.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-run/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | const name = 'Get run'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: name, 10 | value: name, 11 | action: name, 12 | description: 13 | 'Gets the details of a specific Actor run by its ID. This endpoint is useful for retrieving information about a run, such as its status, storages, and other metadata.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties, name }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Run actor'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Run an Actor', 10 | value: 'Run actor', 11 | action: 'Run an Actor', 12 | description: 13 | 'Runs an Actor. You can override the Actor’s input configuration by providing a custom body, which will override the prefilled input values.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/Apify.properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | import { properties as resources } from './resources'; 3 | 4 | const authenticationProperties: INodeProperties[] = [ 5 | { 6 | displayName: 'Authentication', 7 | name: 'authentication', 8 | type: 'options', 9 | options: [ 10 | { 11 | name: 'API Key', 12 | value: 'apifyApi', 13 | }, 14 | { 15 | name: 'OAuth2', 16 | value: 'apifyOAuth2Api', 17 | }, 18 | ], 19 | default: 'apifyApi', 20 | description: 'Choose which authentication method to use', 21 | }, 22 | ]; 23 | 24 | export const properties: INodeProperties[] = [...resources, ...authenticationProperties]; 25 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor-and-get-dataset/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Run actor and get dataset'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Run an Actor and Get Dataset', 10 | value: 'Run actor and get dataset', 11 | action: 'Run an Actor and get dataset', 12 | description: 'Runs an Actor, waits for it to finish, and finally returns the dataset items', 13 | }; 14 | 15 | const { properties, option } = runHooks(rawOption, rawProperties); 16 | 17 | export { option, properties }; 18 | -------------------------------------------------------------------------------- /icons/apify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nodes/Apify/apify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/scrape-single-url/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const scrapeSingleUrlName = 'Scrape single URL'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Scrape Single URL', 10 | value: scrapeSingleUrlName, 11 | action: scrapeSingleUrlName, 12 | description: 13 | 'Scrape a single URL using the Apify Website Content Crawler Actor and get its content as text, markdown, and HTML', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-user-runs-list/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Get user runs list'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Get User Runs List', 10 | value: 'Get user runs list', 11 | action: 'Get user runs list', 12 | description: 13 | 'Gets a list of Actor runs for the user. This endpoint is useful for retrieving a history of runs, their statuses, and other data.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/get-last-run/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Get last run'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Get Last Run', 10 | value: 'Get last run', 11 | action: 'Get last run', 12 | description: 13 | 'Retrieves the most recent run of an Actor. This endpoint is useful for quickly accessing the latest run details, including its status and storages, without needing to specify a run ID.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /credentials/ApifyApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import type { IAuthenticateGeneric, ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | export class ApifyApi implements ICredentialType { 4 | name = 'apifyApi'; 5 | 6 | displayName = 'Apify API'; 7 | 8 | documentationUrl = 'https://docs.apify.com/platform/integrations/api#api-token'; 9 | 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'API Key', 13 | name: 'apiKey', 14 | type: 'string', 15 | typeOptions: { password: true }, 16 | default: '', 17 | }, 18 | ]; 19 | 20 | authenticate: IAuthenticateGeneric = { 21 | type: 'generic', 22 | properties: { 23 | headers: { 24 | Authorization: '=Bearer {{$credentials.apiKey}}', 25 | }, 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task-and-get-dataset/index.ts: -------------------------------------------------------------------------------- 1 | import { INodePropertyOptions } from 'n8n-workflow'; 2 | 3 | import { properties as rawProperties } from './properties'; 4 | import { runHooks } from './hooks'; 5 | 6 | export const name = 'Run task and get dataset'; 7 | 8 | const rawOption: INodePropertyOptions = { 9 | name: 'Run Task and Get Dataset', 10 | value: 'Run task and get dataset', 11 | action: 'Run task and get dataset', 12 | description: 13 | 'Runs an Actor task, waits for it to finish, and finally returns the dataset items. You can optionally override the Actor’s input configuration by providing a custom body.', 14 | }; 15 | 16 | const { properties, option } = runHooks(rawOption, rawProperties); 17 | 18 | export { option, properties }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/" 23 | }, 24 | "include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"], 25 | "exclude": ["nodes/Apify/__tests__"] 26 | } 27 | -------------------------------------------------------------------------------- /nodes/Apify/Apify.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-apify", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": [ 6 | "Development", 7 | "Marketing & Content", 8 | "Sales", 9 | "Analytics", 10 | "Utility", 11 | "Data & Storage" 12 | ], 13 | "resources": { 14 | "credentialDocumentation": [ 15 | { 16 | "url": "https://docs.apify.com/platform/integrations/n8n" 17 | } 18 | ], 19 | "primaryDocumentation": [ 20 | { 21 | "url": "https://docs.apify.com/" 22 | } 23 | ] 24 | }, 25 | "alias": [ 26 | "apify", 27 | "actor", 28 | "serp", 29 | "llm", 30 | "ai agent", 31 | "tavily", 32 | "mcp server", 33 | "scrape", 34 | "crawler", 35 | "web", 36 | "dataset", 37 | "bright", 38 | "search", 39 | "data extraction", 40 | "proxy", 41 | "fire", 42 | "automation" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /nodes/Apify/helpers/consts.ts: -------------------------------------------------------------------------------- 1 | export const WEB_CONTENT_SCRAPER_ACTOR_ID = 'aYG0l9s7dbB7j3gbS'; 2 | export const APIFY_API_URL = 'https://api.apify.com'; 3 | export const TERMINAL_RUN_STATUSES = ['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED']; 4 | export const WAIT_FOR_FINISH_POLL_INTERVAL = 1000; 5 | 6 | export const memoryOptions = [ 7 | { name: '128 MB', value: 128 }, 8 | { name: '256 MB', value: 256 }, 9 | { name: '512 MB', value: 512 }, 10 | { name: '1024 MB (1 GB)', value: 1024 }, 11 | { name: '2048 MB (2 GB)', value: 2048 }, 12 | { name: '4096 MB (4 GB)', value: 4096 }, 13 | { name: '8192 MB (8 GB)', value: 8192 }, 14 | { name: '16384 MB (16 GB)', value: 16384 }, 15 | { name: '32768 MB (32 GB)', value: 32768 }, 16 | ]; 17 | 18 | export const DEFAULT_EXP_BACKOFF_INTERVAL = 1; 19 | export const DEFAULT_EXP_BACKOFF_EXPONENTIAL = 2; 20 | export const DEFAULT_EXP_BACKOFF_RETRIES = 5; 21 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/get-last-run/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest } from '../../../resources/genericFunctions'; 8 | 9 | export async function getLastRun(this: IExecuteFunctions, i: number): Promise { 10 | const actorId = this.getNodeParameter('userActorId', i) as { value: string }; 11 | const status = this.getNodeParameter('status', i) as string; 12 | 13 | if (!actorId) { 14 | throw new NodeOperationError(this.getNode(), 'Actor ID is required'); 15 | } 16 | 17 | try { 18 | const apiResult = await apiRequest.call(this, { 19 | method: 'GET', 20 | uri: `/v2/acts/${actorId.value}/runs/last`, 21 | qs: { status }, 22 | }); 23 | 24 | return { json: { ...apiResult.data } }; 25 | } catch (error) { 26 | throw new NodeApiError(this.getNode(), error); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actors/scrape-single-url.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Scrape single URL workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "operation": "Scrape single URL" 7 | }, 8 | "id": "cde14951-f922-4710-b3c5-dc913fdcc10b", 9 | "name": "Scrape single URL", 10 | "type": "n8n-nodes-apify.apify", 11 | "typeVersion": 1, 12 | "position": [920, 460], 13 | "credentials": { 14 | "apifyApi": { 15 | "id": "dJAynKkN2pRqy3Ko", 16 | "name": "Apify account" 17 | } 18 | } 19 | } 20 | ], 21 | "pinData": {}, 22 | "connections": {}, 23 | "active": false, 24 | "settings": { 25 | "executionOrder": "v1" 26 | }, 27 | "versionId": "9395946a-a8de-48e7-8d04-c84b1274d95f", 28 | "meta": { 29 | "templateCredsSetupCompleted": true, 30 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 31 | }, 32 | "id": "OFLCq8UoIVInQv2Y", 33 | "tags": [] 34 | } 35 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-user-runs-list/execute.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeApiError } from 'n8n-workflow'; 2 | import { apiRequest } from '../../../resources/genericFunctions'; 3 | 4 | export async function getUserRunsList( 5 | this: IExecuteFunctions, 6 | i: number, 7 | ): Promise { 8 | const offset = this.getNodeParameter('offset', i, 0) as number; 9 | const limit = this.getNodeParameter('limit', i, 50) as number; 10 | const desc = this.getNodeParameter('desc', i) as boolean; 11 | const status = this.getNodeParameter('status', i) as string; 12 | 13 | try { 14 | const apiResult = await apiRequest.call(this, { 15 | method: 'GET', 16 | uri: '/v2/actor-runs', 17 | qs: { limit, offset, desc, status }, 18 | }); 19 | 20 | return this.helpers.returnJsonArray(apiResult.data?.items ?? []); 21 | } catch (error) { 22 | throw new NodeApiError(this.getNode(), error); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/datasets/get-items.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get items workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Datasets", 7 | "operation": "Get items", 8 | "datasetId": "WkzbQMuFYuamGv3YF" 9 | }, 10 | "id": "e43c03ec-2a6b-4be3-a40b-1a5e695f4612", 11 | "name": "Get items", 12 | "type": "n8n-nodes-apify.apify", 13 | "typeVersion": 1, 14 | "position": [1000, 460], 15 | "credentials": { 16 | "apifyApi": { 17 | "id": "dJAynKkN2pRqy3Ko", 18 | "name": "Apify account" 19 | } 20 | } 21 | } 22 | ], 23 | "pinData": {}, 24 | "connections": {}, 25 | "active": false, 26 | "settings": { 27 | "executionOrder": "v1" 28 | }, 29 | "versionId": "be6a340f-3901-4f8c-881f-2cbb9a811cdd", 30 | "meta": { 31 | "templateCredsSetupCompleted": true, 32 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 33 | }, 34 | "id": "OFLCq8UoIVInQv2Y", 35 | "tags": [] 36 | } 37 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/get-key-value-store-record/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Key-Value Store ID', 6 | name: 'storeId', 7 | required: true, 8 | description: 'The ID of the Key-Value Store', 9 | default: '', 10 | type: 'string', 11 | displayOptions: { 12 | show: { 13 | resource: ['Key-Value Stores'], 14 | operation: ['Get Key-Value Store Record'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Key-Value Store Record Key', 20 | name: 'recordKey', 21 | required: true, 22 | description: 'The key of the record to be retrieved', 23 | default: 'RECORD_KEY', 24 | type: 'string', 25 | displayOptions: { 26 | hide: { 27 | storeId: [''], // Hide while storeId is not set 28 | }, 29 | show: { 30 | resource: ['Key-Value Stores'], 31 | operation: ['Get Key-Value Store Record'], 32 | }, 33 | }, 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-runs/get-user-runs-list.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get user runs list workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor runs", 7 | "operation": "Get user runs list", 8 | "limit": 2 9 | }, 10 | "id": "ca31ba04-c199-459e-b042-5fec37ea3ccf", 11 | "name": "Get user runs list", 12 | "type": "n8n-nodes-apify.apify", 13 | "typeVersion": 1, 14 | "position": [920, 460], 15 | "credentials": { 16 | "apifyApi": { 17 | "apiToken": "test-token", 18 | "name": "Apify account" 19 | } 20 | } 21 | } 22 | ], 23 | "pinData": {}, 24 | "connections": {}, 25 | "active": false, 26 | "settings": { 27 | "executionOrder": "v1" 28 | }, 29 | "versionId": "b4d4aad1-8314-4211-aa04-63dc0214e565", 30 | "meta": { 31 | "templateCredsSetupCompleted": true, 32 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 33 | }, 34 | "id": "OFLCq8UoIVInQv2Y", 35 | "tags": [] 36 | } 37 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/index.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | import { runHooks } from './hooks'; 3 | 4 | import * as getItems from './get-items'; 5 | 6 | const operations: INodePropertyOptions[] = [getItems.option]; 7 | 8 | export const name = 'Datasets'; 9 | 10 | const operationSelect: INodeProperties = { 11 | displayName: 'Operation', 12 | name: 'operation', 13 | type: 'options', 14 | noDataExpression: true, 15 | displayOptions: { 16 | show: { 17 | resource: ['Datasets'], 18 | }, 19 | }, 20 | default: '', 21 | }; 22 | 23 | // overwrite the options of the operationSelect 24 | operationSelect.options = operations; 25 | 26 | // set the default operation 27 | operationSelect.default = operations.length > 0 ? operations[0].value : ''; 28 | 29 | export const rawProperties: INodeProperties[] = [operationSelect, ...getItems.properties]; 30 | 31 | const { properties, methods } = runHooks(rawProperties); 32 | 33 | export { properties, methods }; 34 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/get-items/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest } from '../../../resources/genericFunctions'; 8 | 9 | export async function getItems(this: IExecuteFunctions, i: number): Promise { 10 | const datasetId = this.getNodeParameter('datasetId', i) as string; 11 | const offset = this.getNodeParameter('offset', i, 0) as number; 12 | const limit = this.getNodeParameter('limit', i, 50) as number; 13 | 14 | if (!datasetId) { 15 | throw new NodeOperationError(this.getNode(), 'Dataset ID is required'); 16 | } 17 | 18 | try { 19 | const itemsArray = await apiRequest.call(this, { 20 | method: 'GET', 21 | uri: `/v2/datasets/${datasetId}/items`, 22 | qs: { offset, limit }, 23 | }); 24 | 25 | return this.helpers.returnJsonArray(itemsArray); 26 | } catch (error) { 27 | throw new NodeApiError(this.getNode(), error); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor/execute.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; 2 | import { executeActor } from '../../executeActor'; 3 | 4 | export async function runActor(this: IExecuteFunctions, i: number): Promise { 5 | const actorId = this.getNodeParameter('actorId', i, undefined, { 6 | extractValue: true, 7 | }) as string; 8 | const timeout = this.getNodeParameter('timeout', i) as number | null; 9 | const memory = this.getNodeParameter('memory', i) as number | null; 10 | const buildParam = this.getNodeParameter('build', i) as string | null; 11 | const waitForFinish = this.getNodeParameter('waitForFinish', i) as boolean; 12 | const rawStringifiedInput = this.getNodeParameter('customBody', i, '{}') as string | object; 13 | 14 | const { lastRunData } = await executeActor.call(this, { 15 | actorId, 16 | timeout, 17 | memory, 18 | buildParam, 19 | rawStringifiedInput, 20 | waitForFinish, 21 | }); 22 | 23 | return { 24 | json: { ...lastRunData }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [22.x] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Enable Corepack 22 | run: corepack enable 23 | 24 | - name: Setup Node.js & cache pnpm 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: pnpm 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Format check 34 | run: pnpm exec prettier --check nodes credentials 35 | 36 | - name: Lint code 37 | run: pnpm lint 38 | 39 | - name: Type-check 40 | run: pnpm tsc --noEmit 41 | 42 | - name: Build project 43 | run: pnpm build 44 | 45 | - name: Run tests 46 | run: pnpm test 47 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as datasetsResourceName } from './index'; 4 | import { name as getItemsOperationName } from './get-items'; 5 | import { getItems } from './get-items/execute'; 6 | 7 | export async function datasetsRouter( 8 | this: IExecuteFunctions, 9 | i: number, 10 | ): Promise { 11 | const resource = this.getNodeParameter('resource', i); 12 | const operation = this.getNodeParameter('operation', i); 13 | 14 | if (resource !== datasetsResourceName) { 15 | throw new NodeOperationError( 16 | this.getNode(), 17 | `Resource ${resource} is not valid for ${datasetsResourceName}. Please use correct resource.`, 18 | ); 19 | } 20 | 21 | switch (operation) { 22 | case getItemsOperationName: 23 | return await getItems.call(this, i); 24 | 25 | default: 26 | throw new NodeOperationError( 27 | this.getNode(), 28 | `Operation ${operation} not found. Please use correct operation.`, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/index.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | import { runHooks } from './hooks'; 3 | 4 | import * as getKeyValueStoreRecord from './get-key-value-store-record'; 5 | 6 | const operations: INodePropertyOptions[] = [getKeyValueStoreRecord.option]; 7 | 8 | const name = 'Key-Value Stores'; 9 | 10 | const operationSelect: INodeProperties = { 11 | displayName: 'Operation', 12 | name: 'operation', 13 | type: 'options', 14 | noDataExpression: true, 15 | displayOptions: { 16 | show: { 17 | resource: ['Key-Value Stores'], 18 | }, 19 | }, 20 | default: '', 21 | }; 22 | 23 | // overwrite the options of the operationSelect 24 | operationSelect.options = operations; 25 | 26 | // set the default operation 27 | operationSelect.default = operations.length > 0 ? operations[0].value : ''; 28 | 29 | export const rawProperties: INodeProperties[] = [ 30 | operationSelect, 31 | ...getKeyValueStoreRecord.properties, 32 | ]; 33 | 34 | const { properties, methods } = runHooks(rawProperties); 35 | 36 | export { properties, methods, name }; 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as keyValueStoreResourceName } from './index'; 4 | import { name as getKeyValueStoreRecordOperationName } from './get-key-value-store-record'; 5 | import { getKeyValueStoreRecord } from './get-key-value-store-record/execute'; 6 | 7 | export async function keyValueStoresRouter( 8 | this: IExecuteFunctions, 9 | i: number, 10 | ): Promise { 11 | const resource = this.getNodeParameter('resource', i); 12 | const operation = this.getNodeParameter('operation', i); 13 | 14 | if (resource !== keyValueStoreResourceName) { 15 | throw new NodeOperationError( 16 | this.getNode(), 17 | `Resource ${resource} is not valid for ${keyValueStoreResourceName}. Please use correct resource.`, 18 | ); 19 | } 20 | 21 | switch (operation) { 22 | case getKeyValueStoreRecordOperationName: 23 | return await getKeyValueStoreRecord.call(this, i); 24 | 25 | default: 26 | throw new NodeOperationError(this.getNode(), `Operation ${operation} not found`); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nodes/Apify/resources/datasets/get-items/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Dataset ID', 6 | name: 'datasetId', 7 | required: true, 8 | description: 'Dataset ID or `username~dataset-name`', 9 | default: '', 10 | type: 'string', 11 | displayOptions: { 12 | show: { 13 | resource: ['Datasets'], 14 | operation: ['Get items'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Offset', 20 | name: 'offset', 21 | description: 'Number of items that should be skipped at the start. The default value is `0`.', 22 | default: null, 23 | type: 'number', 24 | displayOptions: { 25 | show: { 26 | resource: ['Datasets'], 27 | operation: ['Get items'], 28 | }, 29 | }, 30 | }, 31 | { 32 | displayName: 'Limit', 33 | name: 'limit', 34 | description: 'Max number of results to return', 35 | default: 50, 36 | type: 'number', 37 | typeOptions: { 38 | minValue: 1, 39 | }, 40 | displayOptions: { 41 | show: { 42 | resource: ['Datasets'], 43 | operation: ['Get items'], 44 | }, 45 | }, 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/index.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | import { runHooks } from './hooks'; 3 | 4 | import * as runTask from './run-task'; 5 | import * as runTaskAndGetDataset from './run-task-and-get-dataset'; 6 | 7 | const operations: INodePropertyOptions[] = [runTask.option, runTaskAndGetDataset.option]; 8 | 9 | export const name = 'Actor tasks'; 10 | 11 | const operationSelect: INodeProperties = { 12 | displayName: 'Operation', 13 | name: 'operation', 14 | type: 'options', 15 | noDataExpression: true, 16 | displayOptions: { 17 | show: { 18 | resource: ['Actor tasks'], 19 | }, 20 | }, 21 | default: '', 22 | }; 23 | 24 | // overwrite the options of the operationSelect 25 | operationSelect.options = operations; 26 | 27 | // set the default operation 28 | operationSelect.default = operations.length > 0 ? operations[0].value : ''; 29 | 30 | export const rawProperties: INodeProperties[] = [ 31 | operationSelect, 32 | ...runTask.properties, 33 | ...runTaskAndGetDataset.properties, 34 | ]; 35 | 36 | const { properties, methods } = runHooks(rawProperties); 37 | 38 | export { properties, methods }; 39 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/scrape-single-url/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'URL', 6 | name: 'url', 7 | description: 'URL to be scraped. Must start with http:// or https:// and be a valid URL.', 8 | default: 'https://docs.apify.com/academy/web-scraping-for-beginners', 9 | type: 'string', 10 | displayOptions: { 11 | show: { 12 | resource: ['Actors'], 13 | operation: ['Scrape single URL'], 14 | }, 15 | }, 16 | }, 17 | { 18 | displayName: 'Crawler Type', 19 | name: 'crawlerType', 20 | default: 'cheerio', 21 | type: 'options', 22 | options: [ 23 | { 24 | name: 'Cheerio', 25 | value: 'cheerio', 26 | }, 27 | { 28 | name: 'JSDOM', 29 | value: 'jsdom', 30 | }, 31 | { 32 | name: 'Playwright Adaptive', 33 | value: 'playwright:adaptive', 34 | }, 35 | { 36 | name: 'Playwright Firefox', 37 | value: 'playwright:firefox', 38 | }, 39 | ], 40 | displayOptions: { 41 | show: { 42 | resource: ['Actors'], 43 | operation: ['Scrape single URL'], 44 | }, 45 | }, 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-run/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest } from '../../../resources/genericFunctions'; 8 | 9 | export async function getRun(this: IExecuteFunctions, i: number): Promise { 10 | const runId = this.getNodeParameter('runId', i, undefined, { 11 | extractValue: true, 12 | }) as string; 13 | 14 | if (!runId) { 15 | throw new NodeOperationError(this.getNode(), 'Run ID is required'); 16 | } 17 | 18 | try { 19 | const apiResult = await apiRequest.call(this, { 20 | method: 'GET', 21 | uri: `/v2/actor-runs/${runId}`, 22 | }); 23 | 24 | if (!apiResult) { 25 | throw new NodeApiError(this.getNode(), { 26 | message: `Run ${runId} not found`, 27 | }); 28 | } 29 | 30 | if (apiResult.error) { 31 | throw new NodeApiError(this.getNode(), { 32 | message: apiResult.error.message, 33 | type: apiResult.error.type, 34 | }); 35 | } 36 | 37 | return { json: { ...apiResult.data } }; 38 | } catch (error) { 39 | throw new NodeApiError(this.getNode(), error); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-runs/get-run.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get run workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor runs", 7 | "operation": "Get run", 8 | "runId": { 9 | "__rl": true, 10 | "value": "c7Orwz5b830Tbp784", 11 | "mode": "list", 12 | "cachedResultName": "Tue May 20 2025 2:15:19 PM - SUCCEEDED", 13 | "cachedResultUrl": "https://console.apify.com/runs/c7Orwz5b830Tbp784" 14 | } 15 | }, 16 | "id": "8d65b3e8-ff1a-4a7c-b710-1f1952888a93", 17 | "name": "Get run", 18 | "type": "n8n-nodes-apify.apify", 19 | "typeVersion": 1, 20 | "position": [920, 460], 21 | "credentials": { 22 | "apifyApi": { 23 | "apiToken": "test-token", 24 | "name": "Apify account" 25 | } 26 | } 27 | } 28 | ], 29 | "pinData": {}, 30 | "connections": {}, 31 | "active": false, 32 | "settings": { 33 | "executionOrder": "v1" 34 | }, 35 | "versionId": "797464a5-1fd3-43ff-987c-4449acd16f90", 36 | "meta": { 37 | "templateCredsSetupCompleted": true, 38 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 39 | }, 40 | "id": "OFLCq8UoIVInQv2Y", 41 | "tags": [] 42 | } 43 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-runs/get-runs.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get runs workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor runs", 7 | "operation": "Get runs", 8 | "userActorId": { 9 | "__rl": true, 10 | "value": "nFJndFXA5zjCTuudP", 11 | "mode": "list", 12 | "cachedResultName": "Google Search Results Scraper", 13 | "cachedResultUrl": "https://console.apify.com/actors/nFJndFXA5zjCTuudP/input" 14 | } 15 | }, 16 | "type": "n8n-nodes-apify.apify", 17 | "typeVersion": 1, 18 | "position": [220, 0], 19 | "id": "a267e63c-b7b4-4546-815e-6af63afb4230", 20 | "name": "Get runs", 21 | "credentials": { 22 | "apifyApi": { 23 | "id": "KffSIpaKJqYH0jXh", 24 | "name": "Apify account" 25 | } 26 | } 27 | } 28 | ], 29 | "pinData": {}, 30 | "connections": {}, 31 | "active": false, 32 | "settings": { 33 | "executionOrder": "v1" 34 | }, 35 | "versionId": "b0814fb4-4c2c-41ae-bcf5-7c220e7cf7fd", 36 | "meta": { 37 | "templateCredsSetupCompleted": true, 38 | "instanceId": "98d77b2901fb2f0adcd7c8a9c142688dfbe314db559018e4a7becf49b08cd2bf" 39 | }, 40 | "id": "KZcIgxAot0tOOOOG", 41 | "tags": [] 42 | } 43 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actors/get-last-run.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get last run workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "operation": "Get last run", 7 | "userActorId": { 8 | "__rl": true, 9 | "value": "nFJndFXA5zjCTuudP", 10 | "mode": "list", 11 | "cachedResultName": "Google Search Results Scraper", 12 | "cachedResultUrl": "https://console.com/actors/nFJndFXA5zjCTuudP/input" 13 | }, 14 | "status": "ABORTED" 15 | }, 16 | "id": "874a4468-0c56-4818-baa0-41967a17550a", 17 | "name": "Get last run", 18 | "type": "n8n-nodes-apify.apify", 19 | "typeVersion": 1, 20 | "position": [920, 460], 21 | "credentials": { 22 | "apifyApi": { 23 | "id": "dJAynKkN2pRqy3Ko", 24 | "name": "Apify account" 25 | } 26 | } 27 | } 28 | ], 29 | "pinData": {}, 30 | "connections": {}, 31 | "active": false, 32 | "settings": { 33 | "executionOrder": "v1" 34 | }, 35 | "versionId": "8c899a80-bc40-4d52-a418-ac341a836931", 36 | "meta": { 37 | "templateCredsSetupCompleted": true, 38 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 39 | }, 40 | "id": "OFLCq8UoIVInQv2Y", 41 | "tags": [] 42 | } 43 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/webhook/webhook.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Webhook Workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "actorId": { 7 | "__rl": true, 8 | "value": "nFJndFXA5zjCTuudP", 9 | "mode": "list", 10 | "cachedResultName": "Google Search Results Scraper (apify/google-search-scraper)", 11 | "cachedResultUrl": "https://console.apify.com/actors/nFJndFXA5zjCTuudP/input" 12 | } 13 | }, 14 | "id": "498adf8f-f3a2-4a54-a268-699254e71221", 15 | "name": "Apify Trigger", 16 | "type": "n8n-nodes-apify.apifyTrigger", 17 | "typeVersion": 1, 18 | "position": [1820, 580], 19 | "webhookId": "2726981e-4e01-461f-a548-1f467e997400", 20 | "credentials": { 21 | "apifyApi": { 22 | "id": "dJAynKkN2pRqy3Ko", 23 | "name": "Apify account" 24 | } 25 | } 26 | } 27 | ], 28 | "pinData": {}, 29 | "connections": {}, 30 | "active": false, 31 | "settings": { 32 | "executionOrder": "v1" 33 | }, 34 | "versionId": "e3f6ba0a-56cf-4d23-bdf2-0718cad2b0dd", 35 | "meta": { 36 | "templateCredsSetupCompleted": true, 37 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 38 | }, 39 | "id": "OFLCq8UoIVInQv2Y", 40 | "tags": [] 41 | } 42 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-tasks/run-task.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run task no wait workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor tasks", 7 | "operation": "Run task", 8 | "actorTaskId": { 9 | "__rl": true, 10 | "value": "PwUDLcG3zMyT8E4vq", 11 | "mode": "list", 12 | "cachedResultName": "Google Search Results Scraper (Task)", 13 | "cachedResultUrl": "https://console.apify.com/actors/tasks/PwUDLcG3zMyT8E4vq/input" 14 | }, 15 | "waitForFinish": false 16 | }, 17 | "id": "2ada004c-bc23-435d-a65c-371ebcafb4f5", 18 | "name": "Run task", 19 | "type": "n8n-nodes-apify.apify", 20 | "typeVersion": 1, 21 | "position": [920, 460], 22 | "credentials": { 23 | "apifyApi": { 24 | "id": "dJAynKkN2pRqy3Ko", 25 | "name": "Apify account" 26 | } 27 | } 28 | } 29 | ], 30 | "pinData": {}, 31 | "connections": {}, 32 | "active": false, 33 | "settings": { 34 | "executionOrder": "v1" 35 | }, 36 | "versionId": "29897fc5-8618-4467-9665-358072641546", 37 | "meta": { 38 | "templateCredsSetupCompleted": true, 39 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 40 | }, 41 | "id": "OFLCq8UoIVInQv2Y", 42 | "tags": [] 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/index.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | import { runHooks } from './hooks'; 3 | 4 | import * as getUserRunsList from './get-user-runs-list'; 5 | import * as getRun from './get-run'; 6 | import * as getActorRuns from './get-actor-runs'; 7 | 8 | const operations: INodePropertyOptions[] = [ 9 | getUserRunsList.option, 10 | getRun.option, 11 | getActorRuns.option, 12 | ]; 13 | 14 | const name = 'Actor runs'; 15 | 16 | const operationSelect: INodeProperties = { 17 | displayName: 'Operation', 18 | name: 'operation', 19 | type: 'options', 20 | noDataExpression: true, 21 | displayOptions: { 22 | show: { 23 | resource: ['Actor runs'], 24 | }, 25 | }, 26 | default: '', 27 | }; 28 | 29 | // overwrite the options of the operationSelect 30 | operationSelect.options = operations; 31 | 32 | // set the default operation 33 | operationSelect.default = operations.length > 0 ? operations[0].value : ''; 34 | 35 | export const rawProperties: INodeProperties[] = [ 36 | operationSelect, 37 | ...getUserRunsList.properties, 38 | ...getRun.properties, 39 | ...getActorRuns.properties, 40 | ]; 41 | 42 | const { properties, methods } = runHooks(rawProperties); 43 | 44 | export { properties, methods, name }; 45 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-tasks/run-task-wait-for-finish.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run task and wait for finish workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor tasks", 7 | "operation": "Run task", 8 | "actorTaskId": { 9 | "__rl": true, 10 | "value": "PwUDLcG3zMyT8E4vq", 11 | "mode": "list", 12 | "cachedResultName": "Google Search Results Scraper (Task)", 13 | "cachedResultUrl": "https://console.apify.com/actors/tasks/PwUDLcG3zMyT8E4vq/input" 14 | }, 15 | "waitForFinish": true 16 | }, 17 | "id": "2ada004c-bc23-435d-a65c-371ebcafb4f5", 18 | "name": "Run task", 19 | "type": "n8n-nodes-apify.apify", 20 | "typeVersion": 1, 21 | "position": [920, 460], 22 | "credentials": { 23 | "apifyApi": { 24 | "id": "dJAynKkN2pRqy3Ko", 25 | "name": "Apify account" 26 | } 27 | } 28 | } 29 | ], 30 | "pinData": {}, 31 | "connections": {}, 32 | "active": false, 33 | "settings": { 34 | "executionOrder": "v1" 35 | }, 36 | "versionId": "29897fc5-8618-4467-9665-358072641546", 37 | "meta": { 38 | "templateCredsSetupCompleted": true, 39 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 40 | }, 41 | "id": "OFLCq8UoIVInQv2Y", 42 | "tags": [] 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-actor-runs/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest } from '../../genericFunctions'; 8 | 9 | export async function getActorRuns( 10 | this: IExecuteFunctions, 11 | i: number, 12 | ): Promise { 13 | const offset = this.getNodeParameter('offset', i, 0) as number; 14 | const limit = this.getNodeParameter('limit', i, 50) as number; 15 | const desc = this.getNodeParameter('desc', i) as boolean; 16 | const status = this.getNodeParameter('status', i) as string; 17 | const statusFilter = status === '' ? undefined : status; 18 | 19 | const actorId = this.getNodeParameter('userActorId', i, undefined, { 20 | extractValue: true, 21 | }) as string; 22 | 23 | if (!actorId) { 24 | throw new NodeOperationError(this.getNode(), 'Actor ID is required'); 25 | } 26 | 27 | try { 28 | const apiResult = await apiRequest.call(this, { 29 | method: 'GET', 30 | uri: `/v2/acts/${actorId}/runs`, 31 | qs: { limit, offset, desc, status: statusFilter }, 32 | }); 33 | 34 | return this.helpers.returnJsonArray(apiResult.data?.items ?? []); 35 | } catch (error) { 36 | throw new NodeApiError(this.getNode(), error); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actors/run-actor.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run actor workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "operation": "Run actor", 7 | "actorId": { 8 | "__rl": true, 9 | "value": "nFJndFXA5zjCTuudP", 10 | "mode": "list", 11 | "cachedResultName": "Google Search Results Scraper", 12 | "cachedResultUrl": "https://console.com/actors/nFJndFXA5zjCTuudP/input" 13 | }, 14 | "waitForFinish": false, 15 | "customBody": "{\n \"maxPagesPerQuery\": 1,\n \"resultsPerPage\": 2,\n \"queries\": \"javascript\\ntypescript\"\n}" 16 | }, 17 | "id": "8b21ab5d-baff-48f1-9512-831704fd2df4", 18 | "name": "Run actor", 19 | "type": "n8n-nodes-apify.apify", 20 | "typeVersion": 1, 21 | "position": [1460, 520], 22 | "credentials": { 23 | "apifyApi": { 24 | "id": "dJAynKkN2pRqy3Ko", 25 | "name": "Apify account" 26 | } 27 | } 28 | } 29 | ], 30 | "pinData": {}, 31 | "connections": {}, 32 | "active": false, 33 | "settings": { 34 | "executionOrder": "v1" 35 | }, 36 | "versionId": "3407ee9f-0aac-4990-804f-702d06bbc85b", 37 | "meta": { 38 | "templateCredsSetupCompleted": true, 39 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 40 | }, 41 | "id": "OFLCq8UoIVInQv2Y", 42 | "tags": [] 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/utils/nodeTypesClass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IDataObject, 3 | INodeType, 4 | INodeTypeData, 5 | INodeTypes, 6 | IVersionedNodeType, 7 | NodeHelpers, 8 | } from 'n8n-workflow'; 9 | import { Apify } from '../../Apify.node'; 10 | import { ApifyTrigger } from '../../ApifyTrigger.node'; 11 | 12 | export class NodeTypesClass implements INodeTypes { 13 | nodeTypes: INodeTypeData = {}; 14 | getByName(nodeType: string): INodeType | IVersionedNodeType { 15 | return this.nodeTypes[nodeType].type; 16 | } 17 | 18 | getKnownTypes(): IDataObject { 19 | return this.nodeTypes; 20 | } 21 | 22 | addNode(nodeTypeName: string, nodeType: INodeType | IVersionedNodeType) { 23 | const loadedNode = { 24 | [nodeTypeName]: { 25 | sourcePath: '', 26 | type: nodeType, 27 | }, 28 | }; 29 | 30 | this.nodeTypes = { 31 | ...this.nodeTypes, 32 | ...loadedNode, 33 | }; 34 | 35 | Object.assign(this.nodeTypes, loadedNode); 36 | } 37 | 38 | getByNameAndVersion(nodeType: string, version?: number): INodeType { 39 | return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version); 40 | } 41 | } 42 | 43 | const nodeTypes = new NodeTypesClass(); 44 | 45 | nodeTypes.addNode('n8n-nodes-apify.apify', new Apify()); 46 | nodeTypes.addNode('n8n-nodes-apify.apifyTrigger', new ApifyTrigger()); 47 | 48 | export { nodeTypes }; 49 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/get-last-run/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Actor', 6 | name: 'userActorId', 7 | required: true, 8 | description: 'Actor ID or a tilde-separated username and Actor name', 9 | default: 'janedoe~my-actor', 10 | type: 'string', 11 | displayOptions: { 12 | show: { 13 | resource: ['Actors'], 14 | operation: ['Get last run'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Status', 20 | name: 'status', 21 | description: 22 | 'Return only runs with the provided status. See the dropdown options or the Apify documentation https://docs.apify.com/platform/actors/running/runs-and-builds#lifecycle.', 23 | default: 'SUCCEEDED', 24 | type: 'options', 25 | options: [ 26 | { name: 'ABORTED', value: 'ABORTED' }, 27 | { name: 'ABORTING', value: 'ABORTING' }, 28 | { name: 'FAILED', value: 'FAILED' }, 29 | { name: 'READY', value: 'READY' }, 30 | { name: 'RUNNING', value: 'RUNNING' }, 31 | { name: 'SUCCEEDED', value: 'SUCCEEDED' }, 32 | { name: 'TIMED-OUT', value: 'TIMED-OUT' }, 33 | { name: 'TIMING-OUT', value: 'TIMING-OUT' }, 34 | ], 35 | displayOptions: { 36 | show: { 37 | resource: ['Actors'], 38 | operation: ['Get last run'], 39 | }, 40 | }, 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actors/run-actor-wait-for-finish.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run actor and wait for finish - workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "operation": "Run actor", 7 | "actorId": { 8 | "__rl": true, 9 | "value": "nFJndFXA5zjCTuudP", 10 | "mode": "list", 11 | "cachedResultName": "Google Search Results Scraper", 12 | "cachedResultUrl": "https://console.com/actors/nFJndFXA5zjCTuudP/input" 13 | }, 14 | "waitForFinish": true, 15 | "customBody": "{\n \"maxPagesPerQuery\": 1,\n \"resultsPerPage\": 2,\n \"queries\": \"javascript\\ntypescript\"\n}" 16 | }, 17 | "id": "8b21ab5d-baff-48f1-9512-831704fd2df4", 18 | "name": "Run actor", 19 | "type": "n8n-nodes-apify.apify", 20 | "typeVersion": 1, 21 | "position": [1460, 520], 22 | "credentials": { 23 | "apifyApi": { 24 | "id": "dJAynKkN2pRqy3Ko", 25 | "name": "Apify account" 26 | } 27 | } 28 | } 29 | ], 30 | "pinData": {}, 31 | "connections": {}, 32 | "active": false, 33 | "settings": { 34 | "executionOrder": "v1" 35 | }, 36 | "versionId": "3407ee9f-0aac-4990-804f-702d06bbc85b", 37 | "meta": { 38 | "templateCredsSetupCompleted": true, 39 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 40 | }, 41 | "id": "OFLCq8UoIVInQv2Y", 42 | "tags": [] 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as actorTaskResourceName } from './index'; 4 | import { name as runTaskOperationName } from './run-task'; 5 | import { name as runTaskAndGetDatasetOperationName } from './run-task-and-get-dataset'; 6 | import { runTask } from './run-task/execute'; 7 | import { runTaskAndGetDataset } from './run-task-and-get-dataset/execute'; 8 | 9 | export async function actorTasksRouter( 10 | this: IExecuteFunctions, 11 | i: number, 12 | ): Promise { 13 | const resource = this.getNodeParameter('resource', i); 14 | const operation = this.getNodeParameter('operation', i); 15 | 16 | if (resource !== actorTaskResourceName) { 17 | throw new NodeOperationError( 18 | this.getNode(), 19 | `Resource ${resource} is not valid for ${actorTaskResourceName}. Please use correct resource.`, 20 | ); 21 | } 22 | 23 | switch (operation) { 24 | case runTaskOperationName: 25 | return await runTask.call(this, i); 26 | 27 | case runTaskAndGetDatasetOperationName: 28 | return await runTaskAndGetDataset.call(this, i); 29 | 30 | default: 31 | throw new NodeOperationError( 32 | this.getNode(), 33 | `Operation ${operation} not found. Please use correct operation.`, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actors/run-actor-and-get-dataset.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run actor and get dataset workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "operation": "Run actor and get dataset", 7 | "actorId": { 8 | "__rl": true, 9 | "value": "nFJndFXA5zjCTuudP", 10 | "mode": "list", 11 | "cachedResultName": "Google Search Results Scraper", 12 | "cachedResultUrl": "https://console.com/actors/nFJndFXA5zjCTuudP/input" 13 | }, 14 | "customBody": "{\n \"maxPagesPerQuery\": 1,\n \"resultsPerPage\": 2,\n \"queries\": \"javascript\\ntypescript\"\n}", 15 | "memory": 1024 16 | }, 17 | "id": "8b21ab5d-baff-48f1-9512-831704fd2df4", 18 | "name": "Run actor and get dataset", 19 | "type": "n8n-nodes-apify.apify", 20 | "typeVersion": 1, 21 | "position": [1460, 520], 22 | "credentials": { 23 | "apifyApi": { 24 | "id": "dJAynKkN2pRqy3Ko", 25 | "name": "Apify account" 26 | } 27 | } 28 | } 29 | ], 30 | "pinData": {}, 31 | "connections": {}, 32 | "active": false, 33 | "settings": { 34 | "executionOrder": "v1" 35 | }, 36 | "versionId": "3407ee9f-0aac-4990-804f-702d06bbc85b", 37 | "meta": { 38 | "templateCredsSetupCompleted": true, 39 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 40 | }, 41 | "id": "OFLCq8UoIVInQv2Y", 42 | "tags": [] 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/index.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; 2 | import { runHooks } from './hooks'; 3 | 4 | import * as runActor from './run-actor'; 5 | import * as scrapeSingleUrl from './scrape-single-url'; 6 | import * as getLastRun from './get-last-run'; 7 | import * as runActorAndGetDataset from './run-actor-and-get-dataset'; 8 | 9 | const operations: INodePropertyOptions[] = [ 10 | runActor.option, 11 | runActorAndGetDataset.option, 12 | scrapeSingleUrl.option, 13 | getLastRun.option, 14 | ]; 15 | 16 | export const name = 'Actors'; 17 | 18 | const operationSelect: INodeProperties = { 19 | displayName: 'Operation', 20 | name: 'operation', 21 | type: 'options', 22 | noDataExpression: true, 23 | displayOptions: { 24 | show: { 25 | resource: ['Actors'], 26 | }, 27 | }, 28 | default: '', 29 | }; 30 | 31 | // overwrite the options of the operationSelect 32 | operationSelect.options = operations; 33 | 34 | // set the default operation 35 | operationSelect.default = operations.length > 0 ? operations[0].value : ''; 36 | 37 | export const rawProperties: INodeProperties[] = [ 38 | operationSelect, 39 | ...runActor.properties, 40 | ...runActorAndGetDataset.properties, 41 | ...scrapeSingleUrl.properties, 42 | ...getLastRun.properties, 43 | ]; 44 | 45 | const { properties, methods } = runHooks(rawProperties); 46 | 47 | export { properties, methods }; 48 | -------------------------------------------------------------------------------- /nodes/Apify/resources/hooks.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, INodeType } from 'n8n-workflow'; 2 | 3 | import { overrideActorProperties, listActors } from './actorResourceLocator'; 4 | import { overrideActorTaskProperties, listActorTasks } from './actorTaskResourceLocator'; 5 | import { overrideUserActorProperties, listUserActors } from './userActorResourceLocator'; 6 | import { overrideRunProperties, listRuns } from './runResourceLocator'; 7 | import { 8 | listKeyValueStores, 9 | overrideKeyValueStoreProperties, 10 | } from './keyValueStoreResourceLocator'; 11 | import { 12 | listKeyValueStoreRecordKeys, 13 | overrideKeyValueStoreRecordKeyProperties, 14 | } from './keyValueStoreRecordKeyResourceLocator'; 15 | import { compose } from './genericFunctions'; 16 | 17 | export function runHooks(properties: INodeProperties[]): { 18 | properties: INodeProperties[]; 19 | methods: INodeType['methods']; 20 | } { 21 | const processProperties = compose( 22 | overrideActorProperties, 23 | overrideActorTaskProperties, 24 | overrideRunProperties, 25 | overrideKeyValueStoreProperties, 26 | overrideKeyValueStoreRecordKeyProperties, 27 | overrideUserActorProperties, 28 | ); 29 | 30 | return { 31 | properties: processProperties(properties), 32 | methods: { 33 | listSearch: { 34 | listActors, 35 | listActorTasks, 36 | listRuns, 37 | listKeyValueStores, 38 | listKeyValueStoreRecordKeys, 39 | listUserActors, 40 | }, 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/actor-tasks/run-task-and-get-dataset.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Run task and get dataset workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Actor tasks", 7 | "operation": "Run task and get dataset", 8 | "actorTaskId": { 9 | "__rl": true, 10 | "value": "PwUDLcG3zMyT8E4vq", 11 | "mode": "list", 12 | "cachedResultName": "Google Search Results Scraper Task", 13 | "cachedResultUrl": "https://console.com/actor-tasks/PwUDLcG3zMyT8E4vq/input" 14 | }, 15 | "useCustomBody": true, 16 | "customBody": "{\n \"maxPagesPerQuery\": 1,\n \"resultsPerPage\": 2,\n \"queries\": \"javascript\\ntypescript\"\n}", 17 | "memory": 1024 18 | }, 19 | "id": "8b21ab5d-baff-48f1-9512-831704fd2df4", 20 | "name": "Run task and get dataset", 21 | "type": "n8n-nodes-apify.apify", 22 | "typeVersion": 1, 23 | "position": [1460, 520], 24 | "credentials": { 25 | "apifyApi": { 26 | "id": "dJAynKkN2pRqy3Ko", 27 | "name": "Apify account" 28 | } 29 | } 30 | } 31 | ], 32 | "pinData": {}, 33 | "connections": {}, 34 | "active": false, 35 | "settings": { 36 | "executionOrder": "v1" 37 | }, 38 | "versionId": "3407ee9f-0aac-4990-804f-702d06bbc85b", 39 | "meta": { 40 | "templateCredsSetupCompleted": true, 41 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 42 | }, 43 | "id": "OFLCq8UoIVInQv2Y", 44 | "tags": [] 45 | } 46 | -------------------------------------------------------------------------------- /nodes/Apify/resources/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as actorRunResourceName } from './actor-runs'; 4 | import { name as actorTaskResourceName } from './actor-tasks'; 5 | import { name as actorResourceName } from './actors'; 6 | import { name as datasetResourceName } from './datasets'; 7 | import { name as keyValueStoreResourceName } from './key-value-stores'; 8 | import { actorRunsRouter } from './actor-runs/router'; 9 | import { actorTasksRouter } from './actor-tasks/router'; 10 | import { actorsRouter } from './actors/router'; 11 | import { datasetsRouter } from './datasets/router'; 12 | import { keyValueStoresRouter } from './key-value-stores/router'; 13 | 14 | export async function resourceRouter( 15 | this: IExecuteFunctions, 16 | i: number, 17 | ): Promise { 18 | const resource = this.getNodeParameter('resource', 0); 19 | 20 | switch (resource) { 21 | case actorRunResourceName: 22 | return await actorRunsRouter.call(this, i); 23 | 24 | case actorTaskResourceName: 25 | return await actorTasksRouter.call(this, i); 26 | 27 | case actorResourceName: 28 | return await actorsRouter.call(this, i); 29 | 30 | case datasetResourceName: 31 | return await datasetsRouter.call(this, i); 32 | 33 | case keyValueStoreResourceName: 34 | return await keyValueStoresRouter.call(this, i); 35 | 36 | default: 37 | throw new NodeOperationError(this.getNode(), `Resource ${resource} not found`); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/workflows/key-value-stores/get-key-value-store-record.workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get Key-Value Store Record Workflow", 3 | "nodes": [ 4 | { 5 | "parameters": { 6 | "resource": "Key-Value Stores", 7 | "operation": "Get Key-Value Store Record", 8 | "storeId": { 9 | "__rl": true, 10 | "value": "yTfMu13hDFe9bRjx6", 11 | "mode": "list", 12 | "cachedResultName": "scrape-single-url-with-screenshot", 13 | "cachedResultUrl": "https://console.apify.com/storage/key-value-stores/yTfMu13hDFe9bRjx6" 14 | }, 15 | "recordKey": { 16 | "__rl": true, 17 | "value": "INPUT", 18 | "mode": "list", 19 | "cachedResultName": "INPUT", 20 | "cachedResultUrl": "https://api.apify.com/v2/key-value-stores/yTfMu13hDFe9bRjx6/records/INPUT" 21 | } 22 | }, 23 | "id": "baa008f9-c785-4a84-84a0-156728c60ef8", 24 | "name": "Get Key-Value Store Record", 25 | "type": "n8n-nodes-apify.apify", 26 | "typeVersion": 1, 27 | "position": [920, 460], 28 | "credentials": { 29 | "apifyApi": { 30 | "id": "dJAynKkN2pRqy3Ko", 31 | "name": "Apify account" 32 | } 33 | } 34 | } 35 | ], 36 | "pinData": {}, 37 | "connections": {}, 38 | "active": false, 39 | "settings": { 40 | "executionOrder": "v1" 41 | }, 42 | "versionId": "a8c6d7de-6912-4f78-9a74-41139dcfcf51", 43 | "meta": { 44 | "templateCredsSetupCompleted": true, 45 | "instanceId": "3f65ba173ae28613be507e94c0a98de4375527c55e31b7fc173a4edee4e2ded3" 46 | }, 47 | "id": "OFLCq8UoIVInQv2Y", 48 | "tags": [] 49 | } 50 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as actorRunResourceName } from './index'; 4 | import { name as getRunOperationName } from './get-run'; 5 | import { name as getUserRunsListOperationName } from './get-user-runs-list'; 6 | import { name as getActorRunsOperationName } from './get-actor-runs'; 7 | import { getRun } from './get-run/execute'; 8 | import { getUserRunsList } from './get-user-runs-list/execute'; 9 | import { getActorRuns } from './get-actor-runs/execute'; 10 | 11 | export async function actorRunsRouter( 12 | this: IExecuteFunctions, 13 | i: number, 14 | ): Promise { 15 | const resource = this.getNodeParameter('resource', i); 16 | const operation = this.getNodeParameter('operation', i); 17 | 18 | if (resource !== actorRunResourceName) { 19 | throw new NodeOperationError( 20 | this.getNode(), 21 | `Resource ${resource} is not valid for ${actorRunResourceName}. Please use correct resource.`, 22 | ); 23 | } 24 | 25 | switch (operation) { 26 | case getRunOperationName: 27 | return await getRun.call(this, i); 28 | 29 | case getUserRunsListOperationName: 30 | return await getUserRunsList.call(this, i); 31 | 32 | case getActorRunsOperationName: 33 | return await getActorRuns.call(this, i); 34 | 35 | default: 36 | throw new NodeOperationError( 37 | this.getNode(), 38 | `Operation ${operation} not found. Please use correct operation.`, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor-and-get-dataset/execute.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeApiError } from 'n8n-workflow'; 2 | import { apiRequest } from '../../genericFunctions'; 3 | import { executeActor } from '../../executeActor'; 4 | 5 | export async function runActorAndGetDataset( 6 | this: IExecuteFunctions, 7 | i: number, 8 | ): Promise { 9 | const actorId = this.getNodeParameter('actorId', i, undefined, { 10 | extractValue: true, 11 | }) as string; 12 | const timeout = this.getNodeParameter('timeout', i) as number | null; 13 | const memory = this.getNodeParameter('memory', i) as number | null; 14 | const buildParam = this.getNodeParameter('build', i) as string | null; 15 | const rawStringifiedInput = this.getNodeParameter('customBody', i, '{}') as string | object; 16 | 17 | const { runId, lastRunData } = await executeActor.call(this, { 18 | actorId, 19 | timeout, 20 | memory, 21 | buildParam, 22 | rawStringifiedInput, 23 | waitForFinish: true, 24 | }); 25 | 26 | if (!lastRunData?.defaultDatasetId) { 27 | throw new NodeApiError(this.getNode(), { 28 | message: `Run ${runId} did not create a dataset`, 29 | }); 30 | } 31 | 32 | if (lastRunData?.status !== 'SUCCEEDED') { 33 | throw new NodeApiError(this.getNode(), { 34 | message: `Run ${runId} did not finish with status SUCCEEDED. Run status: ${lastRunData?.status}`, 35 | }); 36 | } 37 | 38 | const datasetItems = await apiRequest.call(this, { 39 | method: 'GET', 40 | uri: `/v2/datasets/${lastRunData.defaultDatasetId}/items`, 41 | qs: { format: 'json' }, 42 | }); 43 | 44 | return this.helpers.returnJsonArray(datasetItems); 45 | } 46 | -------------------------------------------------------------------------------- /nodes/Apify/resources/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n8n-nodes-base/node-param-options-type-unsorted-items */ 2 | 3 | import { INodeProperties } from 'n8n-workflow'; 4 | 5 | import { aggregateNodeMethods } from '../helpers/methods'; 6 | import { runHooks } from './hooks'; 7 | 8 | import * as actors from './actors'; 9 | import * as actorTasks from './actor-tasks'; 10 | import * as actorRuns from './actor-runs'; 11 | import * as datasets from './datasets'; 12 | import * as keyValueStores from './key-value-stores'; 13 | 14 | const authenticationProperties: INodeProperties[] = []; 15 | 16 | const resourceSelect: INodeProperties[] = [ 17 | { 18 | displayName: 'Resource', 19 | name: 'resource', 20 | type: 'options', 21 | noDataExpression: true, 22 | options: [ 23 | { 24 | name: 'Actor', 25 | value: 'Actors', 26 | }, 27 | { 28 | name: 'Actor Task', 29 | value: 'Actor tasks', 30 | }, 31 | { 32 | name: 'Actor Run', 33 | value: 'Actor runs', 34 | }, 35 | { 36 | name: 'Dataset', 37 | value: 'Datasets', 38 | }, 39 | { 40 | name: 'Key-Value Store', 41 | value: 'Key-Value Stores', 42 | }, 43 | ], 44 | default: 'Actors', 45 | }, 46 | ]; 47 | 48 | const rawProperties: INodeProperties[] = [ 49 | ...authenticationProperties, 50 | ...resourceSelect, 51 | ...actors.properties, 52 | ...actorTasks.properties, 53 | ...actorRuns.properties, 54 | ...datasets.properties, 55 | ...keyValueStores.properties, 56 | ]; 57 | 58 | const { properties, methods: selfMethods } = runHooks(rawProperties); 59 | 60 | const methods = aggregateNodeMethods([ 61 | selfMethods, 62 | actors.methods, 63 | actorTasks.methods, 64 | actorRuns.methods, 65 | datasets.methods, 66 | keyValueStores.methods, 67 | ]); 68 | 69 | export { properties, methods }; 70 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/router.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeOperationError } from 'n8n-workflow'; 2 | 3 | import { name as actorResourceName } from './index'; 4 | import { name as runActorOperationName } from './run-actor'; 5 | import { name as runActorAndGetDatasetOperationName } from './run-actor-and-get-dataset'; 6 | import { scrapeSingleUrlName as scrapeSingleUrlOperationName } from './scrape-single-url'; 7 | import { name as getLastRunOperationName } from './get-last-run'; 8 | import { runActor } from './run-actor/execute'; 9 | import { scrapeSingleUrl } from './scrape-single-url/execute'; 10 | import { getLastRun } from './get-last-run/execute'; 11 | import { runActorAndGetDataset } from './run-actor-and-get-dataset/execute'; 12 | 13 | export async function actorsRouter( 14 | this: IExecuteFunctions, 15 | i: number, 16 | ): Promise { 17 | const resource = this.getNodeParameter('resource', i); 18 | const operation = this.getNodeParameter('operation', i); 19 | 20 | if (resource !== actorResourceName) { 21 | throw new NodeOperationError( 22 | this.getNode(), 23 | `Resource ${resource} is not valid for ${actorResourceName}. Please use correct resource.`, 24 | ); 25 | } 26 | 27 | switch (operation) { 28 | case runActorOperationName: 29 | return await runActor.call(this, i); 30 | case runActorAndGetDatasetOperationName: 31 | return await runActorAndGetDataset.call(this, i); 32 | case scrapeSingleUrlOperationName: 33 | return await scrapeSingleUrl.call(this, i); 34 | case getLastRunOperationName: 35 | return await getLastRun.call(this, i); 36 | 37 | default: 38 | throw new NodeOperationError( 39 | this.getNode(), 40 | `Operation ${operation} not found. Please use correct operation.`, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | root: true, 6 | 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | 13 | parser: '@typescript-eslint/parser', 14 | 15 | parserOptions: { 16 | project: ['./tsconfig.json'], 17 | sourceType: 'module', 18 | extraFileExtensions: ['.json'], 19 | }, 20 | 21 | ignorePatterns: [ 22 | '.eslintrc.js', 23 | '**/*.js', 24 | '**/node_modules/**', 25 | '**/dist/**', 26 | 'nodes/Apify/__tests__/**', 27 | ], 28 | 29 | plugins: ['@stylistic'], 30 | rules: { 31 | '@stylistic/quotes': [ 32 | 'error', 33 | 'single', 34 | { allowTemplateLiterals: 'always', ignoreStringLiterals: true }, 35 | ], 36 | }, 37 | 38 | overrides: [ 39 | { 40 | files: ['package.json'], 41 | plugins: ['eslint-plugin-n8n-nodes-base'], 42 | extends: ['plugin:n8n-nodes-base/community'], 43 | rules: { 44 | 'n8n-nodes-base/community-package-json-name-still-default': 'off', 45 | }, 46 | }, 47 | { 48 | files: ['./credentials/**/*.ts'], 49 | plugins: ['eslint-plugin-n8n-nodes-base'], 50 | extends: ['plugin:n8n-nodes-base/credentials'], 51 | rules: { 52 | 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', 53 | 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', 54 | }, 55 | }, 56 | { 57 | files: ['./nodes/**/*.ts'], 58 | plugins: ['eslint-plugin-n8n-nodes-base'], 59 | extends: ['plugin:n8n-nodes-base/nodes'], 60 | rules: { 61 | 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', 62 | 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', 63 | 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', 64 | 'n8n-nodes-base/node-param-default-wrong-for-options': 'off', 65 | }, 66 | }, 67 | ], 68 | }; 69 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-user-runs-list/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Offset', 6 | name: 'offset', 7 | description: `Number of array elements that should be skipped at the start. The 8 | default value is \`0\`.`, 9 | default: 0, 10 | type: 'number', 11 | displayOptions: { 12 | show: { 13 | resource: ['Actor runs'], 14 | operation: ['Get user runs list'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Limit', 20 | name: 'limit', 21 | description: 'Max number of results to return', 22 | default: 50, 23 | type: 'number', 24 | typeOptions: { 25 | minValue: 1, 26 | }, 27 | displayOptions: { 28 | show: { 29 | resource: ['Actor runs'], 30 | operation: ['Get user runs list'], 31 | }, 32 | }, 33 | }, 34 | { 35 | displayName: 'Desc', 36 | name: 'desc', 37 | description: `Whether the objects are sorted by the \`startedAt\` field in 38 | descending order. By default, they are sorted in ascending order.`, 39 | default: true, 40 | type: 'boolean', 41 | displayOptions: { 42 | show: { 43 | resource: ['Actor runs'], 44 | operation: ['Get user runs list'], 45 | }, 46 | }, 47 | }, 48 | { 49 | displayName: 'Status', 50 | name: 'status', 51 | description: `Return only runs with the provided terminal status, available 52 | statuses: https://docs.apify.com/platform/actors/running/runs-and-builds#lifecycle`, 53 | default: 'SUCCEEDED', 54 | type: 'options', 55 | options: [ 56 | { name: 'SUCCEEDED', value: 'SUCCEEDED' }, 57 | { name: 'FAILED', value: 'FAILED' }, 58 | { name: 'TIMED-OUT', value: 'TIMED-OUT' }, 59 | { name: 'ABORTED', value: 'ABORTED' }, 60 | ], 61 | displayOptions: { 62 | show: { 63 | resource: ['Actor runs'], 64 | operation: ['Get user runs list'], 65 | }, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /credentials/ApifyOAuth2Api.credentials.ts: -------------------------------------------------------------------------------- 1 | import type { ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | const scopes = ['profile', 'full_api_access']; 4 | 5 | export class ApifyOAuth2Api implements ICredentialType { 6 | name = 'apifyOAuth2Api'; 7 | 8 | extends = ['oAuth2Api']; 9 | 10 | displayName = 'Apify OAuth2 API'; 11 | 12 | properties: INodeProperties[] = [ 13 | { 14 | displayName: 'Grant Type', 15 | name: 'grantType', 16 | type: 'hidden', 17 | default: 'pkce', 18 | }, 19 | { 20 | displayName: 'Authorization URL', 21 | name: 'authUrl', 22 | type: 'hidden', 23 | default: 'https://console.apify.com/authorize/oauth', 24 | }, 25 | { 26 | displayName: 'Access Token URL', 27 | name: 'accessTokenUrl', 28 | type: 'hidden', 29 | default: 'https://console-backend.apify.com/oauth/apps/token', 30 | }, 31 | { 32 | displayName: 'Scope (do not change)', 33 | name: 'scope', 34 | type: 'string', 35 | default: `${scopes.join(' ')}`, 36 | noDataExpression: true, 37 | displayOptions: { 38 | hideOnCloud: true, 39 | }, 40 | }, 41 | { 42 | displayName: 'Auth URI Query Parameters', 43 | name: 'authQueryParameters', 44 | type: 'hidden', 45 | default: '', 46 | }, 47 | { 48 | displayName: 'Authentication', 49 | name: 'authentication', 50 | type: 'hidden', 51 | default: 'header', 52 | }, 53 | { 54 | displayName: 'Client ID', 55 | name: 'clientId', 56 | type: 'hidden', 57 | default: '', 58 | }, 59 | { 60 | displayName: 'Client Secret', 61 | name: 'clientSecret', 62 | type: 'hidden', 63 | default: '', 64 | }, 65 | { 66 | displayName: 67 | 'This credential type is not available on self hosted n8n instances, please use an API key instead.', 68 | name: 'notice', 69 | type: 'notice', 70 | default: '', 71 | displayOptions: { 72 | hideOnCloud: true, 73 | }, 74 | }, 75 | ]; 76 | } 77 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/scrape-single-url/execute.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, INodeExecutionData, NodeApiError } from 'n8n-workflow'; 2 | import { apiRequest, pollRunStatus } from '../../../resources/genericFunctions'; 3 | import { consts } from '../../../helpers'; 4 | 5 | export async function scrapeSingleUrl( 6 | this: IExecuteFunctions, 7 | i: number, 8 | ): Promise { 9 | const url = this.getNodeParameter('url', i) as string; 10 | const crawlerType = this.getNodeParameter('crawlerType', i, 'cheerio') as string; 11 | 12 | try { 13 | const input = { 14 | startUrls: [{ url }], 15 | crawlerType, 16 | maxCrawlDepth: 0, 17 | maxCrawlPages: 1, 18 | maxResults: 1, 19 | proxyConfiguration: { 20 | useApifyProxy: true, 21 | }, 22 | removeCookieWarnings: true, 23 | saveHtml: true, 24 | saveMarkdown: true, 25 | }; 26 | 27 | // Run the actor and do not wait for finish 28 | 29 | const run = await apiRequest.call(this, { 30 | method: 'POST', 31 | uri: `/v2/acts/${consts.WEB_CONTENT_SCRAPER_ACTOR_ID}/runs`, 32 | body: input, 33 | qs: { waitForFinish: 0 }, 34 | }); 35 | 36 | const runId = run?.data?.id || run?.id; 37 | 38 | if (!runId) { 39 | throw new NodeApiError(this.getNode(), { 40 | message: 'No run ID returned from actor run', 41 | }); 42 | } 43 | 44 | // Poll for terminal status 45 | const lastRunData = await pollRunStatus.call(this, runId); 46 | 47 | const defaultDatasetId = lastRunData?.defaultDatasetId; 48 | 49 | if (!defaultDatasetId) { 50 | throw new NodeApiError(this.getNode(), { 51 | message: 'No dataset ID returned from actor run', 52 | }); 53 | } 54 | 55 | const [item] = await apiRequest.call(this, { 56 | method: 'GET', 57 | uri: `/v2/datasets/${defaultDatasetId}/items`, 58 | qs: { format: 'json' }, 59 | }); 60 | 61 | delete item.text; 62 | 63 | return { json: { ...item } }; 64 | } catch (error) { 65 | throw new NodeApiError(this.getNode(), error); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest, customBodyParser, pollRunStatus } from '../../../resources/genericFunctions'; 8 | 9 | export async function runTask(this: IExecuteFunctions, i: number): Promise { 10 | const actorTaskId = this.getNodeParameter('actorTaskId', i, undefined, { 11 | extractValue: true, 12 | }) as string; 13 | const rawStringifiedInput = this.getNodeParameter('customBody', i, '{}') as string | object; 14 | const waitForFinish = this.getNodeParameter('waitForFinish', i) as boolean; 15 | const timeout = this.getNodeParameter('timeout', i, null) as number | null; 16 | const memory = this.getNodeParameter('memory', i, null) as number | null; 17 | const build = this.getNodeParameter('build', i, '') as string; 18 | 19 | let input: any; 20 | try { 21 | input = customBodyParser(rawStringifiedInput); 22 | } catch (err) { 23 | throw new NodeOperationError( 24 | this.getNode(), 25 | `Could not parse custom body: ${rawStringifiedInput}`, 26 | ); 27 | } 28 | 29 | if (!actorTaskId) { 30 | throw new NodeOperationError(this.getNode(), 'Task ID is required'); 31 | } 32 | 33 | const qs: Record = {}; 34 | if (timeout != null) qs.timeout = timeout; 35 | if (memory != null) qs.memory = memory; 36 | if (build) qs.build = build; 37 | qs.waitForFinish = 0; // always start run without waiting 38 | 39 | const apiResult = await apiRequest.call(this, { 40 | method: 'POST', 41 | uri: `/v2/actor-tasks/${actorTaskId}/runs`, 42 | body: input, 43 | qs, 44 | }); 45 | 46 | if (!apiResult?.data?.id) { 47 | throw new NodeApiError(this.getNode(), { 48 | message: `Run ID not found after running the task`, 49 | }); 50 | } 51 | 52 | if (!waitForFinish) { 53 | return { json: { ...apiResult.data } }; 54 | } 55 | 56 | const runId = apiResult.data.id; 57 | const lastRunData = await pollRunStatus.call(this, runId); 58 | return { json: { ...lastRunData } }; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apify/n8n-nodes-apify", 3 | "version": "0.6.4", 4 | "description": "n8n nodes for Apify", 5 | "keywords": [ 6 | "n8n-community-node-package", 7 | "n8n-nodes", 8 | "n8n", 9 | "apify", 10 | "apify-node", 11 | "apify-nodes" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/apify/n8n-nodes-apify", 15 | "author": { 16 | "name": "Apify Team", 17 | "email": "integrations@apify.com" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/apify/n8n-nodes-apify.git" 22 | }, 23 | "engines": { 24 | "node": ">=18.10", 25 | "pnpm": ">=9.1" 26 | }, 27 | "packageManager": "pnpm@9.1.4", 28 | "main": "index.js", 29 | "scripts": { 30 | "preinstall": "npx only-allow pnpm", 31 | "build": "tsc && gulp build:icons", 32 | "dev": "tsc --watch", 33 | "format": "prettier nodes credentials --write", 34 | "lint": "eslint nodes credentials package.json", 35 | "lintfix": "eslint nodes credentials package.json --fix", 36 | "prepublishOnly": "pnpm build && pnpm lint -c .eslintrc.prepublish.js nodes credentials package.json", 37 | "merge:api": "npx openapi-merge-cli --config ./openapi.config.json", 38 | "test": "WEBHOOK_URL=https://localhost:5678 jest --config jest.config.js" 39 | }, 40 | "files": [ 41 | "dist" 42 | ], 43 | "n8n": { 44 | "n8nNodesApiVersion": 1, 45 | "credentials": [ 46 | "dist/credentials/ApifyApi.credentials.js", 47 | "dist/credentials/ApifyOAuth2Api.credentials.js" 48 | ], 49 | "nodes": [ 50 | "dist/nodes/Apify/Apify.node.js", 51 | "dist/nodes/Apify/ApifyTrigger.node.js" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@stylistic/eslint-plugin": "^4.2.0", 56 | "@types/jest": "^29.5.14", 57 | "@types/node": "^24.0.1", 58 | "@typescript-eslint/parser": "^7.15.0", 59 | "eslint": "^8.56.0", 60 | "eslint-plugin-n8n-nodes-base": "^1.16.1", 61 | "gulp": "^4.0.2", 62 | "jest": "^29.7.0", 63 | "n8n-core": "1.83.0", 64 | "n8n-workflow": "1.82.0", 65 | "nock": "^14.0.5", 66 | "prettier": "^3.3.2", 67 | "ts-jest": "^29.3.2", 68 | "typescript": "5.5.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /nodes/Apify/resources/keyValueStoreResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Key-Value Store ID', 6 | name: 'storeId', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listKeyValueStores', 17 | searchFilterRequired: false, 18 | searchable: false, 19 | }, 20 | }, 21 | { 22 | displayName: 'ID', 23 | name: 'id', 24 | type: 'string', 25 | validation: [ 26 | { 27 | type: 'regex', 28 | properties: { 29 | regex: '[a-zA-Z0-9]+', 30 | errorMessage: 'Not a valid Key-Value Store ID', 31 | }, 32 | }, 33 | ], 34 | placeholder: 'dmXls2mjfQVdzfrC6', 35 | url: '=https://console.apify.com/storage/key-value-stores/{{ $value }}', 36 | }, 37 | ], 38 | }; 39 | 40 | function mapProperty(property: INodeProperties) { 41 | return { 42 | ...property, 43 | ...resourceLocatorProperty, 44 | }; 45 | } 46 | export function overrideKeyValueStoreProperties(properties: INodeProperties[]) { 47 | return properties.map((property) => { 48 | if (property.name === 'storeId') { 49 | return mapProperty(property); 50 | } 51 | return property; 52 | }); 53 | } 54 | 55 | export async function listKeyValueStores( 56 | this: ILoadOptionsFunctions, 57 | ): Promise { 58 | const searchResults = await apiRequestAllItems.call(this, { 59 | method: 'GET', 60 | uri: '/v2/key-value-stores', 61 | qs: { 62 | limit: 100, 63 | offset: 0, 64 | }, 65 | }); 66 | 67 | const { 68 | data: { items }, 69 | } = searchResults; 70 | 71 | return { 72 | results: items.map((b: any) => ({ 73 | name: b.name, 74 | value: b.id, 75 | url: `https://console.apify.com/storage/key-value-stores/${b.id}`, 76 | description: b.name, 77 | })), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /README_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-_node-name_ 2 | 3 | This is an n8n community node. It lets you use _app/service name_ in your n8n workflows. 4 | 5 | _App/service name_ is _one or two sentences describing the service this node integrates with_. 6 | 7 | [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform. 8 | 9 | [Installation](#installation) 10 | [Operations](#operations) 11 | [Credentials](#credentials) 12 | [Compatibility](#compatibility) 13 | [Usage](#usage) 14 | [Resources](#resources) 15 | [Version history](#version-history) 16 | 17 | ## Installation 18 | 19 | Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation. 20 | 21 | ## Operations 22 | 23 | _List the operations supported by your node._ 24 | 25 | ## Credentials 26 | 27 | _If users need to authenticate with the app/service, provide details here. You should include prerequisites (such as signing up with the service), available authentication methods, and how to set them up._ 28 | 29 | ## Compatibility 30 | 31 | _State the minimum n8n version, as well as which versions you test against. You can also include any known version incompatibility issues._ 32 | 33 | ## Usage 34 | 35 | _This is an optional section. Use it to help users with any difficult or confusing aspects of the node._ 36 | 37 | _By the time users are looking for community nodes, they probably already know n8n basics. But if you expect new users, you can link to the [Try it out](https://docs.n8n.io/try-it-out/) documentation to help them get started._ 38 | 39 | ## Resources 40 | 41 | * [n8n community nodes documentation](https://docs.n8n.io/integrations/community-nodes/) 42 | * _Link to app/service documentation._ 43 | 44 | ## Version history 45 | 46 | _This is another optional section. If your node has multiple versions, include a short description of available versions and what changed, as well as any compatibility impact._ 47 | 48 | 49 | -------------------------------------------------------------------------------- /nodes/Apify/Apify.node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n8n-nodes-base/node-class-description-outputs-wrong */ 2 | /* eslint-disable n8n-nodes-base/node-class-description-inputs-wrong-regular-node */ 3 | 4 | import { 5 | IExecuteFunctions, 6 | INodeExecutionData, 7 | INodeType, 8 | INodeTypeDescription, 9 | NodeConnectionType, 10 | } from 'n8n-workflow'; 11 | import { properties } from './Apify.properties'; 12 | import { methods } from './Apify.methods'; 13 | import { resourceRouter } from './resources/router'; 14 | 15 | export class Apify implements INodeType { 16 | description: INodeTypeDescription = { 17 | displayName: 'Apify', 18 | name: 'apify', 19 | icon: 'file:apify.svg', 20 | group: ['transform'], 21 | version: 1, 22 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 23 | description: 'Access Apify tools for web scraping, data extraction, and automation.', 24 | defaults: { 25 | name: 'Apify', 26 | }, 27 | inputs: [NodeConnectionType.Main], 28 | outputs: [NodeConnectionType.Main], 29 | usableAsTool: true, 30 | credentials: [ 31 | { 32 | displayName: 'Apify API key connection', 33 | name: 'apifyApi', 34 | required: false, 35 | displayOptions: { 36 | show: { 37 | authentication: ['apifyApi'], 38 | }, 39 | }, 40 | }, 41 | { 42 | displayName: 'Apify OAuth2 connection', 43 | name: 'apifyOAuth2Api', 44 | required: false, 45 | displayOptions: { 46 | show: { 47 | authentication: ['apifyOAuth2Api'], 48 | }, 49 | }, 50 | }, 51 | ], 52 | 53 | properties, 54 | }; 55 | 56 | methods = methods; 57 | 58 | async execute(this: IExecuteFunctions) { 59 | const items = this.getInputData(); 60 | const returnData: INodeExecutionData[] = []; 61 | 62 | for (let i = 0; i < items.length; i++) { 63 | const data = await resourceRouter.call(this, i); 64 | // `data` may be an array of items or a single item, so we either push the spreaded array or the single item 65 | if (Array.isArray(data)) { 66 | returnData.push(...data); 67 | } else { 68 | returnData.push(data); 69 | } 70 | } 71 | 72 | return [returnData]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/utils/executeWorkflow.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDeferredPromise, 3 | ICredentialsHelper, 4 | IExecuteWorkflowInfo, 5 | IRun, 6 | IWorkflowBase, 7 | IWorkflowExecuteAdditionalData, 8 | LoggerProxy, 9 | Workflow, 10 | } from 'n8n-workflow'; 11 | import { WorkflowExecute, ExecutionLifecycleHooks } from 'n8n-core'; 12 | import { nodeTypes } from './nodeTypesClass'; 13 | 14 | export type ExecuteWorkflowArgs = { 15 | workflow: any; 16 | credentialsHelper: ICredentialsHelper; 17 | }; 18 | 19 | export const executeWorkflow = async ({ credentialsHelper, ...args }: ExecuteWorkflowArgs) => { 20 | LoggerProxy.init({ 21 | debug() {}, 22 | error() {}, 23 | info() {}, 24 | warn() {}, 25 | }); 26 | 27 | const workflow = new Workflow({ 28 | id: 'test', 29 | active: true, 30 | connections: args.workflow.connections, 31 | nodes: args.workflow.nodes, 32 | nodeTypes, 33 | }); 34 | 35 | const waitPromise = createDeferredPromise(); 36 | 37 | const workflowData: IWorkflowBase = { 38 | id: 'test', 39 | name: 'test', 40 | createdAt: new Date(), 41 | updatedAt: new Date(), 42 | active: true, 43 | nodes: args.workflow.nodes, 44 | connections: args.workflow.connections, 45 | }; 46 | 47 | const additionalData: IWorkflowExecuteAdditionalData = { 48 | credentialsHelper, 49 | hooks: new ExecutionLifecycleHooks('trigger', '1', workflowData), 50 | executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise => {}, 51 | restApiUrl: 'http://localhost:5678', 52 | webhookBaseUrl: 'http://localhost:5678', 53 | webhookWaitingBaseUrl: 'http://localhost:5678', 54 | webhookTestBaseUrl: 'http://localhost:5678', 55 | userId: 'userId', 56 | instanceBaseUrl: 'http://localhost:5678', 57 | formWaitingBaseUrl: 'http://localhost:5678', 58 | variables: {}, 59 | secretsHelpers: {} as any, 60 | logAiEvent: async () => {}, 61 | startRunnerTask: (async () => {}) as any, 62 | }; 63 | 64 | const workflowExecute = new WorkflowExecute(additionalData, 'cli'); 65 | 66 | const executionData = await workflowExecute.run(workflow); 67 | 68 | return { 69 | workflow, 70 | waitPromise, 71 | executionData, 72 | additionalData, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /nodes/Apify/resources/keyValueStoreRecordKeyResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Key-Value Store Record Key', 6 | name: 'recordKey', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listKeyValueStoreRecordKeys', 17 | searchFilterRequired: false, 18 | searchable: false, 19 | }, 20 | }, 21 | { 22 | displayName: 'Key', 23 | 24 | name: 'key', 25 | type: 'string', 26 | validation: [ 27 | { 28 | type: 'regex', 29 | properties: { 30 | regex: '.+', 31 | errorMessage: 'Please provide a Record Key', 32 | }, 33 | }, 34 | ], 35 | placeholder: 'RECORD_KEY', 36 | url: '=https://api.apify.com/v2/key-value-stores/{{ $value }}/records/{{ $value }}', 37 | }, 38 | ], 39 | }; 40 | 41 | function mapProperty(property: INodeProperties) { 42 | return { 43 | ...property, 44 | ...resourceLocatorProperty, 45 | }; 46 | } 47 | export function overrideKeyValueStoreRecordKeyProperties(properties: INodeProperties[]) { 48 | return properties.map((property) => { 49 | if (property.name === 'recordKey') { 50 | return mapProperty(property); 51 | } 52 | return property; 53 | }); 54 | } 55 | 56 | export async function listKeyValueStoreRecordKeys( 57 | this: ILoadOptionsFunctions, 58 | ): Promise { 59 | const storeIdParam = this.getNodeParameter('storeId', null) as { value: string }; 60 | 61 | const searchResults = await apiRequestAllItems.call(this, { 62 | method: 'GET', 63 | uri: `/v2/key-value-stores/${storeIdParam.value}/keys`, 64 | qs: { 65 | limit: 100, 66 | offset: 0, 67 | }, 68 | }); 69 | 70 | const { 71 | data: { items }, 72 | } = searchResults; 73 | 74 | return { 75 | results: items.map((b: any) => ({ 76 | name: b.key, 77 | value: b.key, 78 | url: `https://api.apify.com/v2/key-value-stores/${storeIdParam.value}/records/${b.key}`, 79 | description: b.key, 80 | })), 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/utils/credentialHelper.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from 'n8n-core'; 2 | import { 3 | ICredentialDataDecryptedObject, 4 | ICredentialsHelper, 5 | IHttpRequestHelper, 6 | IHttpRequestOptions, 7 | INode, 8 | INodeCredentialsDetails, 9 | ICredentials, 10 | INodeProperties, 11 | IWorkflowExecuteAdditionalData, 12 | WorkflowExecuteMode, 13 | IExecuteData, 14 | ICredentialsExpressionResolveValues, 15 | Workflow, 16 | IRequestOptionsSimplified, 17 | } from 'n8n-workflow'; 18 | 19 | export class CredentialsHelper extends ICredentialsHelper { 20 | private credentials: Record; 21 | 22 | constructor(credentials: Record) { 23 | super(); 24 | this.credentials = credentials; 25 | } 26 | 27 | getParentTypes(name: string): string[] { 28 | return []; 29 | } 30 | 31 | async authenticate( 32 | credentials: ICredentialDataDecryptedObject, 33 | typeName: string, 34 | requestOptions: IHttpRequestOptions | IRequestOptionsSimplified, 35 | workflow: Workflow, 36 | node: INode, 37 | ): Promise { 38 | return requestOptions as IHttpRequestOptions; 39 | } 40 | 41 | async preAuthentication( 42 | helpers: IHttpRequestHelper, 43 | credentials: ICredentialDataDecryptedObject, 44 | typeName: string, 45 | node: INode, 46 | credentialsExpired: boolean, 47 | ): Promise { 48 | return undefined; 49 | } 50 | 51 | async getCredentials( 52 | nodeCredentials: INodeCredentialsDetails, 53 | type: string, 54 | ): Promise { 55 | return new Credentials({ id: null, name: '' }, '', ''); 56 | } 57 | 58 | async getDecrypted( 59 | additionalData: IWorkflowExecuteAdditionalData, 60 | nodeCredentials: INodeCredentialsDetails, 61 | type: string, 62 | mode: WorkflowExecuteMode, 63 | executeData?: IExecuteData, 64 | raw?: boolean, 65 | expressionResolveValues?: ICredentialsExpressionResolveValues, 66 | ): Promise { 67 | return this.credentials[type]; 68 | } 69 | 70 | async updateCredentials( 71 | nodeCredentials: INodeCredentialsDetails, 72 | type: string, 73 | data: ICredentialDataDecryptedObject, 74 | ): Promise {} 75 | 76 | getCredentialsProperties(type: string): INodeProperties[] { 77 | return []; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-runs/get-actor-runs/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const properties: INodeProperties[] = [ 4 | { 5 | displayName: 'Actor', 6 | name: 'userActorId', 7 | required: true, 8 | description: 'Actor ID or a tilde-separated username and Actor name', 9 | default: 'janedoe~my-actor', 10 | type: 'string', 11 | displayOptions: { 12 | show: { 13 | resource: ['Actor runs'], 14 | operation: ['Get runs'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Offset', 20 | name: 'offset', 21 | description: `Number of array elements that should be skipped at the start. The 22 | default value is \`0\`.`, 23 | default: 0, 24 | type: 'number', 25 | displayOptions: { 26 | show: { 27 | resource: ['Actor runs'], 28 | operation: ['Get runs'], 29 | }, 30 | }, 31 | }, 32 | { 33 | displayName: 'Limit', 34 | name: 'limit', 35 | description: 'Max number of results to return', 36 | default: 50, 37 | type: 'number', 38 | typeOptions: { 39 | minValue: 1, 40 | }, 41 | displayOptions: { 42 | show: { 43 | resource: ['Actor runs'], 44 | operation: ['Get runs'], 45 | }, 46 | }, 47 | }, 48 | { 49 | displayName: 'Desc', 50 | name: 'desc', 51 | description: `Whether the objects are sorted by the \`startedAt\` field in 52 | descending order. By default, they are sorted in ascending order.`, 53 | default: true, 54 | type: 'boolean', 55 | displayOptions: { 56 | show: { 57 | resource: ['Actor runs'], 58 | operation: ['Get runs'], 59 | }, 60 | }, 61 | }, 62 | { 63 | displayName: 'Status', 64 | name: 'status', 65 | description: `Return only runs with the provided status, available 66 | statuses: https://docs.apify.com/platform/actors/running/runs-and-builds#lifecycle`, 67 | default: '', 68 | type: 'options', 69 | options: [ 70 | { name: 'ABORTED', value: 'ABORTED' }, 71 | { name: 'ABORTING', value: 'ABORTING' }, 72 | { name: 'All', value: '' }, 73 | { name: 'FAILED', value: 'FAILED' }, 74 | { name: 'READY', value: 'READY' }, 75 | { name: 'RUNNING', value: 'RUNNING' }, 76 | { name: 'SUCCEEDED', value: 'SUCCEEDED' }, 77 | { name: 'TIMED-OUT', value: 'TIMED-OUT' }, 78 | { name: 'TIMING-OUT', value: 'TIMING-OUT' }, 79 | ], 80 | displayOptions: { 81 | show: { 82 | resource: ['Actor runs'], 83 | operation: ['Get runs'], 84 | }, 85 | }, 86 | }, 87 | ]; 88 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task-and-get-dataset/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | NodeApiError, 5 | NodeOperationError, 6 | } from 'n8n-workflow'; 7 | import { apiRequest, customBodyParser, pollRunStatus } from '../../genericFunctions'; 8 | 9 | export async function runTaskAndGetDataset( 10 | this: IExecuteFunctions, 11 | i: number, 12 | ): Promise { 13 | const actorTaskId = this.getNodeParameter('actorTaskId', i, undefined, { 14 | extractValue: true, 15 | }) as string; 16 | const rawStringifiedInput = this.getNodeParameter('customBody', i, '{}') as string | object; 17 | const timeout = this.getNodeParameter('timeout', i, null) as number | null; 18 | const memory = this.getNodeParameter('memory', i, null) as number | null; 19 | const build = this.getNodeParameter('build', i, '') as string; 20 | 21 | let input: any; 22 | try { 23 | input = customBodyParser(rawStringifiedInput); 24 | } catch (err) { 25 | throw new NodeOperationError( 26 | this.getNode(), 27 | `Could not parse custom body: ${rawStringifiedInput}`, 28 | ); 29 | } 30 | 31 | if (!actorTaskId) { 32 | throw new NodeOperationError(this.getNode(), 'Task ID is required'); 33 | } 34 | 35 | const qs: Record = {}; 36 | if (timeout != null) qs.timeout = timeout; 37 | if (memory != null) qs.memory = memory; 38 | if (build) qs.build = build; 39 | qs.waitForFinish = 0; // always start run without waiting 40 | 41 | const apiResult = await apiRequest.call(this, { 42 | method: 'POST', 43 | uri: `/v2/actor-tasks/${actorTaskId}/runs`, 44 | body: input, 45 | qs, 46 | }); 47 | 48 | if (!apiResult?.data?.id) { 49 | throw new NodeApiError(this.getNode(), { 50 | message: `Run ID not found after running the task`, 51 | }); 52 | } 53 | 54 | const runId = apiResult.data.id; 55 | const lastRunData = await pollRunStatus.call(this, runId); 56 | 57 | if (!lastRunData?.defaultDatasetId) { 58 | throw new NodeApiError(this.getNode(), { 59 | message: `Run ${runId} did not create a dataset`, 60 | }); 61 | } 62 | 63 | if (lastRunData?.status !== 'SUCCEEDED') { 64 | throw new NodeApiError(this.getNode(), { 65 | message: `Run ${runId} did not finish with status SUCCEEDED. Run status: ${lastRunData?.status}`, 66 | }); 67 | } 68 | 69 | const datasetItems = await apiRequest.call(this, { 70 | method: 'GET', 71 | uri: `/v2/datasets/${lastRunData.defaultDatasetId}/items`, 72 | qs: { format: 'json' }, 73 | }); 74 | 75 | return this.helpers.returnJsonArray(datasetItems); 76 | } 77 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task-and-get-dataset/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | import * as helpers from '../../../helpers'; 4 | 5 | export const properties: INodeProperties[] = [ 6 | { 7 | displayName: 'Actor Task', 8 | name: 'actorTaskId', 9 | required: true, 10 | description: 'Task ID or a tilde-separated username and task name', 11 | default: 'janedoe~my-task', 12 | type: 'string', 13 | displayOptions: { 14 | show: { 15 | resource: ['Actor tasks'], 16 | operation: ['Run task and get dataset'], 17 | }, 18 | }, 19 | }, 20 | { 21 | displayName: 'Use Custom Body', 22 | name: 'useCustomBody', 23 | type: 'boolean', 24 | description: 'Whether to use a custom body', 25 | // default to false since Task should use task-defined input for its Actor 26 | default: false, 27 | displayOptions: { 28 | show: { 29 | resource: ['Actor tasks'], 30 | operation: ['Run task and get dataset'], 31 | }, 32 | }, 33 | }, 34 | { 35 | displayName: 'Input (JSON)', 36 | name: 'customBody', 37 | type: 'json', 38 | default: '{}', 39 | description: 'Custom body to send', 40 | displayOptions: { 41 | show: { 42 | useCustomBody: [true], 43 | resource: ['Actor tasks'], 44 | operation: ['Run task and get dataset'], 45 | }, 46 | }, 47 | }, 48 | { 49 | displayName: 'Timeout', 50 | name: 'timeout', 51 | description: `Optional timeout for the run, in seconds. By default, the run uses a 52 | timeout specified in the task settings.`, 53 | default: null, 54 | type: 'number', 55 | displayOptions: { 56 | show: { 57 | resource: ['Actor tasks'], 58 | operation: ['Run task and get dataset'], 59 | }, 60 | }, 61 | }, 62 | { 63 | displayName: 'Memory', 64 | name: 'memory', 65 | description: 66 | 'Memory limit for the run, in megabytes. The amount of memory can be set to one of the available options. By default, the run uses a memory limit specified in the task settings.', 67 | default: 1024, 68 | type: 'options', 69 | options: helpers.consts.memoryOptions, 70 | displayOptions: { 71 | show: { 72 | resource: ['Actor tasks'], 73 | operation: ['Run task and get dataset'], 74 | }, 75 | }, 76 | }, 77 | { 78 | displayName: 'Build', 79 | name: 'build', 80 | description: `Specifies the Actor build to run. It can be either a build tag or build 81 | number. By default, the run uses the build specified in the task 82 | settings (typically \`latest\`).`, 83 | default: '', 84 | type: 'string', 85 | displayOptions: { 86 | show: { 87 | resource: ['Actor tasks'], 88 | operation: ['Run task and get dataset'], 89 | }, 90 | }, 91 | }, 92 | ]; 93 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor-and-get-dataset/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | import * as helpers from '../../../helpers'; 4 | 5 | export const properties: INodeProperties[] = [ 6 | { 7 | displayName: 'Actor Source', 8 | name: 'actorSource', 9 | type: 'hidden', 10 | default: 'recentlyUsed', 11 | displayOptions: { 12 | show: { 13 | resource: ['Actors'], 14 | operation: ['Run actor and get dataset'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Actor', 20 | name: 'actorId', 21 | required: true, 22 | description: 'Actor ID or a tilde-separated username and Actor name', 23 | default: 'janedoe~my-actor', 24 | type: 'string', 25 | displayOptions: { 26 | show: { 27 | resource: ['Actors'], 28 | operation: ['Run actor and get dataset'], 29 | }, 30 | }, 31 | }, 32 | { 33 | displayName: 'Input JSON', 34 | name: 'customBody', 35 | type: 'json', 36 | default: '{}', 37 | description: 38 | 'JSON input for the Actor run, which you can find on the Actor input page in Apify Console. If empty, the run uses the input specified in the default run configuration. https://console.apify.com', 39 | displayOptions: { 40 | show: { 41 | resource: ['Actors'], 42 | operation: ['Run actor and get dataset'], 43 | }, 44 | }, 45 | }, 46 | { 47 | displayName: 'Timeout', 48 | name: 'timeout', 49 | description: `Optional timeout for the run, in seconds. By default, the run uses a 50 | timeout specified in the default run configuration for the Actor.`, 51 | default: null, 52 | type: 'number', 53 | displayOptions: { 54 | show: { 55 | resource: ['Actors'], 56 | operation: ['Run actor and get dataset'], 57 | }, 58 | }, 59 | }, 60 | { 61 | displayName: 'Memory', 62 | name: 'memory', 63 | description: 64 | 'Memory limit for the run, in megabytes. The amount of memory can be set to one of the available options. By default, the run uses a memory limit specified in the default run configuration for the Actor.', 65 | default: 1024, 66 | type: 'options', 67 | options: helpers.consts.memoryOptions, 68 | displayOptions: { 69 | show: { 70 | resource: ['Actors'], 71 | operation: ['Run actor and get dataset'], 72 | }, 73 | }, 74 | }, 75 | { 76 | displayName: 'Build Tag', 77 | name: 'build', 78 | description: `Specifies the Actor build tag to run. By default, the run uses the build specified in the default run 79 | configuration for the Actor (typically \`latest\`).`, 80 | default: '', 81 | type: 'string', 82 | displayOptions: { 83 | show: { 84 | resource: ['Actors'], 85 | operation: ['Run actor and get dataset'], 86 | }, 87 | }, 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /nodes/Apify/resources/key-value-stores/get-key-value-store-record/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | IHttpRequestMethods, 4 | INodeExecutionData, 5 | NodeApiError, 6 | NodeOperationError, 7 | } from 'n8n-workflow'; 8 | import { consts } from '../../../helpers'; 9 | import { retryWithExponentialBackoff } from '../../genericFunctions'; 10 | 11 | export async function getKeyValueStoreRecord( 12 | this: IExecuteFunctions, 13 | i: number, 14 | ): Promise { 15 | const storeId = this.getNodeParameter('storeId', i) as { value: string }; 16 | const recordKey = this.getNodeParameter('recordKey', i) as { value: string }; 17 | 18 | if (!storeId || !recordKey) { 19 | throw new NodeOperationError(this.getNode(), 'Store ID and Record Key are required'); 20 | } 21 | 22 | try { 23 | const apiCallFn = () => 24 | this.helpers.httpRequestWithAuthentication.call(this, 'apifyApi', { 25 | method: 'GET' as IHttpRequestMethods, 26 | url: `${consts.APIFY_API_URL}/v2/key-value-stores/${storeId.value}/records/${recordKey.value}`, 27 | headers: { 28 | 'x-apify-integration-platform': 'n8n', 29 | }, 30 | returnFullResponse: true, 31 | encoding: 'arraybuffer', 32 | }); 33 | const apiResult = await retryWithExponentialBackoff(apiCallFn); 34 | 35 | if (!apiResult) { 36 | return { json: {} }; 37 | } 38 | 39 | const contentType = apiResult.headers['content-type'] as string; 40 | const value = apiResult.body; 41 | 42 | const resultBase = { 43 | storeId: storeId.value, 44 | recordKey: recordKey.value, 45 | contentType, 46 | }; 47 | 48 | // If not JSON or text, treat as binary 49 | if ( 50 | contentType && 51 | !contentType.startsWith('application/json') && 52 | !contentType.startsWith('text/') 53 | ) { 54 | const fileName = recordKey.value || apiResult.key || 'file'; 55 | 56 | const binaryData = await this.helpers.prepareBinaryData(value, fileName, contentType); 57 | return { 58 | json: { ...resultBase }, 59 | binary: { data: binaryData }, 60 | }; 61 | } 62 | 63 | // Always get data from buffer for text or JSON 64 | const buffer = value; 65 | let finalData: any; 66 | if (contentType && contentType.startsWith('application/json')) { 67 | try { 68 | finalData = JSON.parse(buffer.toString('utf8')); 69 | } catch { 70 | finalData = buffer.toString('utf8'); 71 | } 72 | } else if (contentType && contentType.startsWith('text/')) { 73 | finalData = buffer.toString('utf8'); 74 | } else { 75 | // fallback: return as base64 string 76 | finalData = buffer.toString('base64'); 77 | } 78 | 79 | return { json: { ...resultBase, data: finalData } }; 80 | } catch (error) { 81 | throw new NodeApiError(this.getNode(), error); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "linterOptions": { 3 | "exclude": [ 4 | "node_modules/**/*" 5 | ] 6 | }, 7 | "defaultSeverity": "error", 8 | "jsRules": {}, 9 | "rules": { 10 | "array-type": [ 11 | true, 12 | "array-simple" 13 | ], 14 | "arrow-return-shorthand": true, 15 | "ban": [ 16 | true, 17 | { 18 | "name": "Array", 19 | "message": "tsstyle#array-constructor" 20 | } 21 | ], 22 | "ban-types": [ 23 | true, 24 | [ 25 | "Object", 26 | "Use {} instead." 27 | ], 28 | [ 29 | "String", 30 | "Use 'string' instead." 31 | ], 32 | [ 33 | "Number", 34 | "Use 'number' instead." 35 | ], 36 | [ 37 | "Boolean", 38 | "Use 'boolean' instead." 39 | ] 40 | ], 41 | "class-name": true, 42 | "curly": [ 43 | true, 44 | "ignore-same-line" 45 | ], 46 | "forin": true, 47 | "jsdoc-format": true, 48 | "label-position": true, 49 | "indent": [ 50 | true, 51 | "tabs", 52 | 2 53 | ], 54 | "member-access": [ 55 | true, 56 | "no-public" 57 | ], 58 | "new-parens": true, 59 | "no-angle-bracket-type-assertion": true, 60 | "no-any": true, 61 | "no-arg": true, 62 | "no-conditional-assignment": true, 63 | "no-construct": true, 64 | "no-debugger": true, 65 | "no-default-export": true, 66 | "no-duplicate-variable": true, 67 | "no-inferrable-types": true, 68 | "ordered-imports": [ 69 | true, 70 | { 71 | "import-sources-order": "any", 72 | "named-imports-order": "case-insensitive" 73 | } 74 | ], 75 | "no-namespace": [ 76 | true, 77 | "allow-declarations" 78 | ], 79 | "no-reference": true, 80 | "no-string-throw": true, 81 | "no-unused-expression": true, 82 | "no-var-keyword": true, 83 | "object-literal-shorthand": true, 84 | "only-arrow-functions": [ 85 | true, 86 | "allow-declarations", 87 | "allow-named-functions" 88 | ], 89 | "prefer-const": true, 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always", 94 | "ignore-bound-class-methods" 95 | ], 96 | "switch-default": true, 97 | "trailing-comma": [ 98 | true, 99 | { 100 | "multiline": { 101 | "objects": "always", 102 | "arrays": "always", 103 | "functions": "always", 104 | "typeLiterals": "ignore" 105 | }, 106 | "esSpecCompliant": true 107 | } 108 | ], 109 | "triple-equals": [ 110 | true, 111 | "allow-null-check" 112 | ], 113 | "use-isnan": true, 114 | "quotes": [ 115 | "error", 116 | "single" 117 | ], 118 | "variable-name": [ 119 | true, 120 | "check-format", 121 | "ban-keywords", 122 | "allow-leading-underscore", 123 | "allow-trailing-underscore" 124 | ] 125 | }, 126 | "rulesDirectory": [] 127 | } 128 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release & Publish Apify n8n Node 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build_and_release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} 18 | fetch-depth: 0 19 | 20 | - name: Enable Corepack 21 | run: corepack enable 22 | 23 | - name: Set up Node.js & cache pnpm 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: '22.x' 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Build project 33 | run: pnpm build 34 | 35 | - name: Run tests 36 | run: pnpm test 37 | 38 | - name: Configure Git 39 | run: | 40 | git config --global user.name "Apify Release Bot" 41 | git config --global user.email "noreply@apify.com" 42 | 43 | - name: Extract release version 44 | id: get_version 45 | run: | 46 | FULL_TAG_NAME="${{ github.event.release.tag_name }}" 47 | VERSION_NUMBER="${FULL_TAG_NAME#v}" 48 | echo "VERSION=${VERSION_NUMBER}" >> $GITHUB_ENV 49 | 50 | - name: Update package.json version 51 | run: | 52 | echo "Updating package.json to version ${{ env.VERSION }}" 53 | pnpm version ${{ env.VERSION }} --no-git-tag-version 54 | 55 | - name: Commit version update 56 | run: | 57 | git add package.json pnpm-lock.yaml 58 | if ! git diff --staged --quiet; then 59 | TARGET_BRANCH="${{ github.event.release.target_commitish }}" 60 | echo "Target branch for push: $TARGET_BRANCH" 61 | git commit -m "chore(release): set version to ${{ env.VERSION }} [skip ci]" 62 | echo "Pushing version update to branch: $TARGET_BRANCH" 63 | git push origin HEAD:"refs/heads/$TARGET_BRANCH" 64 | else 65 | echo "No changes to commit in package.json or pnpm-lock.yaml for version update." 66 | fi 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} 69 | 70 | - name: Set up npm for publishing 71 | run: echo "//registry.npmjs.org/:_authToken=${{secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}" > .npmrc 72 | 73 | - name: Publish to npm 74 | env: 75 | NODE_AUTH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }} 76 | run: | 77 | if pnpm view @apify/n8n-nodes-apify@${{ env.VERSION }} version > /dev/null 2>&1; then 78 | echo "Version ${{ env.VERSION }} of @apify/n8n-nodes-apify is already published to npm. Skipping publish step." 79 | else 80 | echo "Publishing @apify/n8n-nodes-apify@${{ env.VERSION }} to npm..." 81 | pnpm publish --access public --no-git-checks 82 | fi 83 | -------------------------------------------------------------------------------- /nodes/Apify/resources/runResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Run ID', 6 | name: 'runId', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listRuns', 17 | searchFilterRequired: false, 18 | searchable: false, 19 | }, 20 | }, 21 | { 22 | displayName: 'By URL', 23 | name: 'url', 24 | type: 'string', 25 | placeholder: 'https://console.apify.com/actors/runs/RDfcScrqIYHW0jfNF#output', 26 | validation: [ 27 | { 28 | type: 'regex', 29 | properties: { 30 | // https://console.apify.com/actors/runs/RDfcScrqIYHW0jfNF#output 31 | regex: 'https://console.apify.com/actors(/[a-zA-Z0-9]+)?/runs/([a-zA-Z0-9]+).*', 32 | errorMessage: 'Not a valid Apify Actor Run URL', 33 | }, 34 | }, 35 | ], 36 | extractValue: { 37 | type: 'regex', 38 | // https://console.apify.com/actors/runs/RDfcScrqIYHW0jfNF#output -> RDfcScrqIYHW0jfNF 39 | regex: 'https://console.apify.com/actors(?:/[a-zA-Z0-9]+)?/runs/([a-zA-Z0-9]+).*', 40 | }, 41 | }, 42 | { 43 | displayName: 'ID', 44 | name: 'id', 45 | type: 'string', 46 | validation: [ 47 | { 48 | type: 'regex', 49 | properties: { 50 | regex: '[a-zA-Z0-9]+', 51 | errorMessage: 'Not a valid Apify Actor run ID', 52 | }, 53 | }, 54 | ], 55 | placeholder: 'WAtmhr6rhfBnwqKDY', 56 | url: '=http:/console.apify.com/actors/{{ $value }}/runs/{{ $value }}#log', 57 | }, 58 | ], 59 | }; 60 | 61 | function mapProperty(property: INodeProperties) { 62 | return { 63 | ...property, 64 | ...resourceLocatorProperty, 65 | }; 66 | } 67 | export function overrideRunProperties(properties: INodeProperties[]) { 68 | return properties.map((property) => { 69 | if (property.name === 'runId') { 70 | return mapProperty(property); 71 | } 72 | return property; 73 | }); 74 | } 75 | 76 | export async function listRuns(this: ILoadOptionsFunctions): Promise { 77 | const searchResults = await apiRequestAllItems.call(this, { 78 | method: 'GET', 79 | uri: '/v2/actor-runs', 80 | qs: { 81 | limit: 100, 82 | offset: 0, 83 | desc: 1, 84 | }, 85 | }); 86 | 87 | const { data } = searchResults; 88 | const { items } = data; 89 | 90 | return { 91 | results: items.map((b: any) => { 92 | const url = `https://console.apify.com/runs/${b.id}`; 93 | 94 | const readableDateTime = b.finishedAt 95 | ? `${new Date(b.finishedAt).toDateString()} ${new Date(b.finishedAt).toLocaleTimeString()}` 96 | : undefined; 97 | 98 | return { 99 | name: [readableDateTime, b.status].filter(Boolean).join(' - '), 100 | value: b.id, 101 | url, 102 | description: b.status, 103 | }; 104 | }), 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actor-tasks/run-task/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | import * as helpers from '../../../helpers'; 4 | 5 | export const properties: INodeProperties[] = [ 6 | { 7 | displayName: 'Actor Task', 8 | name: 'actorTaskId', 9 | required: true, 10 | description: 'Task ID or a tilde-separated username and task name', 11 | default: 'janedoe~my-task', 12 | type: 'string', 13 | displayOptions: { 14 | show: { 15 | resource: ['Actor tasks'], 16 | operation: ['Run task'], 17 | }, 18 | }, 19 | }, 20 | { 21 | displayName: 'Use Custom Body', 22 | name: 'useCustomBody', 23 | type: 'boolean', 24 | description: 'Whether to use a custom body', 25 | // default to false since Task should use task-defined input for its Actor 26 | default: false, 27 | displayOptions: { 28 | show: { 29 | resource: ['Actor tasks'], 30 | operation: ['Run task'], 31 | }, 32 | }, 33 | }, 34 | { 35 | displayName: 'Input (JSON)', 36 | name: 'customBody', 37 | type: 'json', 38 | default: '{}', 39 | description: 'Custom body to send', 40 | displayOptions: { 41 | show: { 42 | useCustomBody: [true], 43 | resource: ['Actor tasks'], 44 | operation: ['Run task'], 45 | }, 46 | }, 47 | }, 48 | { 49 | displayName: 'Wait for Finish', 50 | name: 'waitForFinish', 51 | description: 52 | 'Whether or not to wait for the run to finish before continuing. If true, the node will wait for the run to complete (successfully or not) before moving to the next node. Note: The maximum time the workflow will wait is limited by the workflow timeout setting in your n8n configuration.', 53 | default: true, 54 | type: 'boolean', 55 | displayOptions: { 56 | show: { 57 | resource: ['Actor tasks'], 58 | operation: ['Run task'], 59 | }, 60 | }, 61 | }, 62 | { 63 | displayName: 'Timeout', 64 | name: 'timeout', 65 | description: `Optional timeout for the run, in seconds. By default, the run uses a 66 | timeout specified in the task settings.`, 67 | default: null, 68 | type: 'number', 69 | displayOptions: { 70 | show: { 71 | resource: ['Actor tasks'], 72 | operation: ['Run task'], 73 | }, 74 | }, 75 | }, 76 | { 77 | displayName: 'Memory', 78 | name: 'memory', 79 | description: 80 | 'Memory limit for the run, in megabytes. The amount of memory can be set to one of the available options. By default, the run uses a memory limit specified in the task settings.', 81 | default: 1024, 82 | type: 'options', 83 | options: helpers.consts.memoryOptions, 84 | displayOptions: { 85 | show: { 86 | resource: ['Actor tasks'], 87 | operation: ['Run task'], 88 | }, 89 | }, 90 | }, 91 | { 92 | displayName: 'Build', 93 | name: 'build', 94 | description: `Specifies the Actor build to run. It can be either a build tag or build 95 | number. By default, the run uses the build specified in the task 96 | settings (typically \`latest\`).`, 97 | default: '', 98 | type: 'string', 99 | displayOptions: { 100 | show: { 101 | resource: ['Actor tasks'], 102 | operation: ['Run task'], 103 | }, 104 | }, 105 | }, 106 | ]; 107 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actors/run-actor/properties.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | import * as helpers from '../../../helpers'; 4 | 5 | export const properties: INodeProperties[] = [ 6 | { 7 | displayName: 'Actor Source', 8 | name: 'actorSource', 9 | type: 'hidden', 10 | default: 'recentlyUsed', 11 | displayOptions: { 12 | show: { 13 | resource: ['Actors'], 14 | operation: ['Run actor'], 15 | }, 16 | }, 17 | }, 18 | { 19 | displayName: 'Actor', 20 | name: 'actorId', 21 | required: true, 22 | description: 'Actor ID or a tilde-separated username and Actor name', 23 | default: 'janedoe~my-actor', 24 | type: 'string', 25 | displayOptions: { 26 | show: { 27 | resource: ['Actors'], 28 | operation: ['Run actor'], 29 | }, 30 | }, 31 | }, 32 | { 33 | displayName: 'Input JSON', 34 | name: 'customBody', 35 | type: 'json', 36 | default: '{}', 37 | description: 38 | 'JSON input for the Actor run, which you can find on the Actor input page in Apify Console. If empty, the run uses the input specified in the default run configuration. https://console.apify.com', 39 | displayOptions: { 40 | show: { 41 | resource: ['Actors'], 42 | operation: ['Run actor'], 43 | }, 44 | }, 45 | }, 46 | { 47 | displayName: 'Wait for Finish', 48 | name: 'waitForFinish', 49 | description: 50 | 'Whether or not to wait for the run to finish before continuing. If true, the node will wait for the run to complete (successfully or not) before moving to the next node. Note: The maximum time the workflow will wait is limited by the workflow timeout setting in your n8n configuration.', 51 | default: true, 52 | type: 'boolean', 53 | displayOptions: { 54 | show: { 55 | resource: ['Actors'], 56 | operation: ['Run actor'], 57 | }, 58 | }, 59 | }, 60 | { 61 | displayName: 'Timeout', 62 | name: 'timeout', 63 | description: `Optional timeout for the run, in seconds. By default, the run uses a 64 | timeout specified in the default run configuration for the Actor.`, 65 | default: null, 66 | type: 'number', 67 | displayOptions: { 68 | show: { 69 | resource: ['Actors'], 70 | operation: ['Run actor'], 71 | }, 72 | }, 73 | }, 74 | { 75 | displayName: 'Memory', 76 | name: 'memory', 77 | description: 78 | 'Memory limit for the run, in megabytes. The amount of memory can be set to one of the available options. By default, the run uses a memory limit specified in the default run configuration for the Actor.', 79 | default: 1024, 80 | type: 'options', 81 | options: helpers.consts.memoryOptions, 82 | displayOptions: { 83 | show: { 84 | resource: ['Actors'], 85 | operation: ['Run actor'], 86 | }, 87 | }, 88 | }, 89 | { 90 | displayName: 'Build Tag', 91 | name: 'build', 92 | description: `Specifies the Actor build tag to run. By default, the run uses the build specified in the default run 93 | configuration for the Actor (typically \`latest\`).`, 94 | default: '', 95 | type: 'string', 96 | displayOptions: { 97 | show: { 98 | resource: ['Actors'], 99 | operation: ['Run actor'], 100 | }, 101 | }, 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actorTaskResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Actor Task', 6 | name: 'actorTaskId', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listActorTasks', 17 | searchFilterRequired: false, 18 | searchable: true, 19 | }, 20 | }, 21 | { 22 | displayName: 'By URL', 23 | name: 'url', 24 | type: 'string', 25 | placeholder: 'https://console.apify.com/actors/tasks/WAtmhr6rhfBnwqKDY/input', 26 | validation: [ 27 | { 28 | type: 'regex', 29 | properties: { 30 | // https://console.apify.com/actors/tasks/WAtmhr6rhfBnwqKDY/input 31 | // https://console.apify.com/actors/tasks/WAtmhr6rhfBnwqKDY 32 | regex: 'https://console.apify.com/actors/tasks/([a-zA-Z0-9]+).*', 33 | errorMessage: 'Not a valid Apify Actor Task URL', 34 | }, 35 | }, 36 | ], 37 | extractValue: { 38 | type: 'regex', 39 | // https://console.apify.com/actors/tasks/WAtmhr6rhfBnwqKDY/input -> WAtmhr6rhfBnwqKDY 40 | // https://console.apify.com/actors/tasks/WAtmhr6rhfBnwqKDY -> WAtmhr6rhfBnwqKDY 41 | regex: 'https://console.apify.com/actors/tasks/([a-zA-Z0-9]+).*', 42 | }, 43 | }, 44 | { 45 | displayName: 'ID', 46 | name: 'id', 47 | type: 'string', 48 | validation: [ 49 | { 50 | type: 'regex', 51 | properties: { 52 | regex: '[a-zA-Z0-9]+', 53 | errorMessage: 'Not a valid Apify Actor Task ID', 54 | }, 55 | }, 56 | ], 57 | placeholder: 'WAtmhr6rhfBnwqKDY', 58 | url: '=http:/console.apify.com/actors/tasks/{{ $value }}/input', 59 | }, 60 | ], 61 | }; 62 | 63 | function mapProperty(property: INodeProperties) { 64 | return { 65 | ...property, 66 | ...resourceLocatorProperty, 67 | }; 68 | } 69 | export function overrideActorTaskProperties(properties: INodeProperties[]) { 70 | return properties.map((property) => { 71 | if (property.name === 'actorTaskId') { 72 | return mapProperty(property); 73 | } 74 | return property; 75 | }); 76 | } 77 | 78 | export async function listActorTasks( 79 | this: ILoadOptionsFunctions, 80 | searchTerm?: string, 81 | ): Promise { 82 | const searchResults = await apiRequestAllItems.call(this, { 83 | method: 'GET', 84 | uri: '/v2/actor-tasks', 85 | qs: { 86 | limit: 100, 87 | offset: 0, 88 | }, 89 | }); 90 | 91 | const { 92 | data: { items }, 93 | } = searchResults; 94 | 95 | let filteredItems = [...items]; 96 | if (searchTerm) { 97 | const regex = new RegExp(searchTerm, 'i'); 98 | filteredItems = items.filter((b: any) => regex.test(b.title || '') || regex.test(b.name || '')); 99 | } 100 | 101 | return { 102 | results: filteredItems.map((b: any) => ({ 103 | name: b.title || b.name, 104 | value: b.id, 105 | url: `https://console.apify.com/actors/tasks/${b.id}/input`, 106 | description: b.name, 107 | })), 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /nodes.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | packageName: 'n8n-nodes-apify', 5 | credentials: { 6 | ApifyApi: { 7 | displayName: 'Apify API key connection', 8 | name: 'apifyApi', 9 | className: 'ApifyApi', 10 | scheme: 'apiKey', 11 | }, 12 | ApifyOAuth2Api: { 13 | displayName: 'Apify OAuth2 connection', 14 | name: 'apifyOAuth2Api', 15 | className: 'ApifyOAuth2Api', 16 | scheme: 'oauth2', 17 | }, 18 | }, 19 | nodes: { 20 | Apify: { 21 | displayName: 'Apify', 22 | name: 'Apify', 23 | description: 'Apify API', 24 | api: path.resolve(__dirname, 'openapi.yaml'), 25 | icon: './icons/apify.svg', 26 | tags: [ 27 | 'Actors/Actor collection', 28 | 'Actors/Actor object', 29 | 'Actors/Run collection', 30 | 'Actors/Run actor synchronously', 31 | 'Actors/Run Actor synchronously and get dataset items', 32 | // 'Actors/Run object', 33 | // 'Actors/Abort run', 34 | // 'Actors/Metamorph run', 35 | // 'Actors/Resurrect run', 36 | 'Actors/Last run object and its storages', 37 | 38 | 'Actor tasks/Task collection', 39 | 'Actor tasks/Task object', 40 | 'Actor tasks/Task input object', 41 | 'Actor tasks/Run collection', 42 | 'Actor tasks/Run task synchronously', 43 | 'Actor tasks/Run task synchronously and get dataset items', 44 | 'Actor tasks/Last run object and its storages', 45 | 46 | 'Actor runs/Run collection', 47 | 'Actor runs/Run object and its storages', 48 | // "Actor runs/Delete run", 49 | // "Actor runs/Abort run", 50 | // "Actor runs/Metamorph run", 51 | // "Actor runs/Reboot run", 52 | // "Actor runs/Resurrect run", 53 | // "Actor runs/Update status message", 54 | 55 | 'Datasets', 56 | 'Datasets/Dataset collection', 57 | 'Datasets/Dataset', 58 | 'Datasets/Item collection', 59 | ], 60 | // operations: ['/v2/acts/{actorId}/run-sync'], 61 | tagsExclude: [], 62 | baseUrl: 'https://api.apify.com', 63 | credentials: [ 64 | { 65 | displayName: 'Apify API key connection', 66 | name: 'apifyApi', 67 | required: false, 68 | show: { 69 | authentication: ['apiKey'], 70 | }, 71 | }, 72 | { 73 | displayName: 'Apify OAuth2 connection', 74 | name: 'apifyOAuth2Api', 75 | required: false, 76 | show: { 77 | authentication: ['oauth2'], 78 | }, 79 | }, 80 | ], 81 | }, 82 | }, 83 | overwrites: { 84 | operations: [ 85 | { 86 | match: { 87 | name: 'resource', 88 | }, 89 | set: function (operation) { 90 | operation.options = operation.options.map((option) => { 91 | return { 92 | ...option, 93 | description: '', 94 | }; 95 | }); 96 | return operation; 97 | }, 98 | }, 99 | { 100 | match: { 101 | name: 'offset', 102 | }, 103 | set: { 104 | default: 0, 105 | }, 106 | }, 107 | ], 108 | }, 109 | operationNameFn: (name) => { 110 | // split by / 111 | // const parts = name.split('/'); 112 | // return parts[1]; 113 | return name; 114 | }, 115 | resourceNameFn: (name, opName) => { 116 | // split by / 117 | const parts = name.split('/'); 118 | return parts[0]; 119 | }, 120 | actionNameFn: (name, opName) => { 121 | // split by / 122 | // const parts = name.split('/'); 123 | return opName; 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jan@n8n.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /nodes/Apify/resources/userActorResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Actor', 6 | name: 'userActorId', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listUserActors', 17 | searchFilterRequired: false, 18 | searchable: true, 19 | }, 20 | }, 21 | { 22 | displayName: 'By URL', 23 | name: 'url', 24 | type: 'string', 25 | placeholder: 'https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input', 26 | validation: [ 27 | { 28 | type: 'regex', 29 | properties: { 30 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input 31 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku 32 | regex: 'https://console.apify.com/actors/([a-zA-Z0-9]+).*', 33 | errorMessage: 'Not a valid Actor URL', 34 | }, 35 | }, 36 | ], 37 | extractValue: { 38 | type: 'regex', 39 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input -> AtBpiepuIUNs2k2ku 40 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku -> AtBpiepuIUNs2k2ku 41 | regex: `https://console.apify.com/actors/([a-zA-Z0-9]+).*`, 42 | }, 43 | }, 44 | { 45 | displayName: 'ID', 46 | name: 'id', 47 | type: 'string', 48 | validation: [ 49 | { 50 | type: 'regex', 51 | properties: { 52 | regex: '[a-zA-Z0-9]+', 53 | errorMessage: 'Not a valid Actor ID', 54 | }, 55 | }, 56 | ], 57 | placeholder: 'NVCnbrChXaPbhVs8bISltEhngFg', 58 | url: '=http:/console.apify.com/actors/{{ $value }}/input', 59 | }, 60 | ], 61 | }; 62 | 63 | function mapProperty(property: INodeProperties): INodeProperties { 64 | return { 65 | ...property, 66 | ...resourceLocatorProperty, 67 | }; 68 | } 69 | 70 | export function overrideUserActorProperties(properties: INodeProperties[]): INodeProperties[] { 71 | const result: INodeProperties[] = []; 72 | 73 | for (const property of properties) { 74 | if (property.name === 'userActorId') { 75 | result.push(mapProperty(property)); 76 | } else { 77 | result.push(property); 78 | } 79 | } 80 | 81 | return result; 82 | } 83 | 84 | export async function listUserActors( 85 | this: ILoadOptionsFunctions, 86 | searchTerm?: string, 87 | ): Promise { 88 | const mapToN8nResult = (actor: any) => ({ 89 | name: actor.title 90 | ? `${actor.title} (${actor.username}/${actor.name})` 91 | : `${actor.username}/${actor.name}`, 92 | value: actor.id, 93 | url: `https://console.apify.com/actors/${actor.id}/input`, 94 | description: actor.description || actor.name, 95 | }); 96 | 97 | const { 98 | data: { items: recentActors }, 99 | } = await apiRequestAllItems.call(this, { 100 | method: 'GET', 101 | uri: '/v2/acts', 102 | qs: { 103 | limit: 1000, 104 | offset: 0, 105 | sortBy: 'stats.lastRunStartedAt', 106 | desc: true, 107 | }, 108 | }); 109 | 110 | if (searchTerm) { 111 | const regex = new RegExp(searchTerm, 'i'); 112 | const filteredActors = recentActors.filter( 113 | (actor: any) => regex.test(actor.title || '') || regex.test(actor.name || ''), 114 | ); 115 | return { 116 | results: filteredActors.map(mapToN8nResult), 117 | }; 118 | } 119 | return { 120 | results: recentActors.map(mapToN8nResult), 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /nodes/Apify/helpers/hooks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BinaryFileType, 3 | IBinaryData, 4 | IDataObject, 5 | IExecuteSingleFunctions, 6 | IHttpRequestOptions, 7 | IN8nHttpFullResponse, 8 | INodeExecutionData, 9 | IPostReceiveBinaryData, 10 | IPostReceiveFilter, 11 | IPostReceiveLimit, 12 | IPostReceiveRootProperty, 13 | IPostReceiveSet, 14 | IPostReceiveSetKeyValue, 15 | IPostReceiveSort, 16 | } from 'n8n-workflow'; 17 | 18 | export async function preSendActionCustonBody( 19 | this: IExecuteSingleFunctions, 20 | requestOptions: IHttpRequestOptions, 21 | ): Promise { 22 | const { customBody } = requestOptions.body as IDataObject; 23 | 24 | if (typeof requestOptions.body === 'object' && typeof customBody === 'object') { 25 | // @ts-ignore 26 | requestOptions.body = { 27 | ...requestOptions.body, 28 | ...customBody, 29 | }; 30 | // @ts-ignore 31 | delete requestOptions.body.customBody; 32 | } 33 | 34 | return Promise.resolve(requestOptions); 35 | } 36 | 37 | /* eslint-disable indent */ 38 | /* tslint:disable:indent */ 39 | export type PostReceiveAction = 40 | | (( 41 | this: IExecuteSingleFunctions, 42 | items: INodeExecutionData[], 43 | response: IN8nHttpFullResponse, 44 | ) => Promise) 45 | | IPostReceiveBinaryData 46 | | IPostReceiveFilter 47 | | IPostReceiveLimit 48 | | IPostReceiveRootProperty 49 | | IPostReceiveSet 50 | | IPostReceiveSetKeyValue 51 | | IPostReceiveSort; 52 | /* eslint-enable indent */ 53 | /* tslint:enable:indent */ 54 | 55 | function getResponseContentType(response: IN8nHttpFullResponse): string { 56 | return response.headers['content-type'] as string; 57 | } 58 | 59 | function getFileTypeFromContentType(contentType: string): string { 60 | const type = contentType.split(';')[0].trim(); 61 | 62 | if (type.includes('/')) { 63 | return type.split('/')[0]; 64 | } 65 | 66 | return type; 67 | } 68 | 69 | function getFileExtensionFromContentType(contentType: string): string { 70 | const type = contentType.split(';')[0].trim(); 71 | 72 | // any/thing -> thing 73 | if (typeof type === 'string' && type.includes('/')) { 74 | return type.split('/')[1]; 75 | } 76 | 77 | return type; 78 | } 79 | 80 | function isBinaryResponse(contentType: string): boolean { 81 | const textContentTypes = [ 82 | /application\/json/, 83 | /application\/xml/, 84 | /application\/xhtml\+xml/, 85 | /application\/atom\+xml/, 86 | /application\/rss\+xml/, 87 | /application\/rdf\+xml/, 88 | /application\/ld\+json/, 89 | /application\/pdf/, 90 | /application\/ld\+json/, 91 | /^text\//, 92 | ]; 93 | 94 | return !textContentTypes.some((regex) => regex.test(contentType)); 95 | } 96 | 97 | export const postReceiveActionBinaryData: PostReceiveAction = 98 | async function postReceiveActionBinaryData( 99 | this: IExecuteSingleFunctions, 100 | items: INodeExecutionData[], 101 | response: IN8nHttpFullResponse, 102 | ): Promise { 103 | const contentType = getResponseContentType(response); 104 | const isBinary = isBinaryResponse(contentType); 105 | 106 | const { binary } = items[0]; 107 | 108 | if (isBinary && binary && binary.data && binary.data.mimeType === 'text/plain') { 109 | const data = binary.data as IBinaryData; 110 | 111 | // convert response body base64 to binary 112 | // @ts-ignore 113 | data.data = Buffer.from(response.body as string, 'binary'); 114 | 115 | data.mimeType = contentType; 116 | data.fileType = getFileTypeFromContentType(contentType) as BinaryFileType; 117 | data.fileExtension = getFileExtensionFromContentType(contentType); 118 | } 119 | 120 | if (binary && binary.data && !binary.data.fileName) { 121 | binary.data.fileName = `data.${getFileExtensionFromContentType(contentType)}`; 122 | } 123 | 124 | return items; 125 | }; 126 | -------------------------------------------------------------------------------- /nodes/Apify/resources/executeActor.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions, NodeApiError, NodeOperationError } from 'n8n-workflow'; 2 | import { apiRequest, customBodyParser, pollRunStatus } from './genericFunctions'; 3 | 4 | export interface ActorExecutionParams { 5 | actorId: string; 6 | timeout: number | null; 7 | memory: number | null; 8 | buildParam: string | null; 9 | rawStringifiedInput: string | object; 10 | waitForFinish?: boolean; 11 | } 12 | 13 | export interface ActorExecutionResult { 14 | runId: string; 15 | lastRunData: any; 16 | } 17 | 18 | export async function executeActor( 19 | this: IExecuteFunctions, 20 | params: ActorExecutionParams, 21 | ): Promise { 22 | const { 23 | actorId, 24 | timeout, 25 | memory, 26 | buildParam, 27 | rawStringifiedInput, 28 | waitForFinish = false, 29 | } = params; 30 | 31 | let userInput: any; 32 | try { 33 | userInput = customBodyParser(rawStringifiedInput); 34 | } catch (err) { 35 | throw new NodeOperationError( 36 | this.getNode(), 37 | `Could not parse custom body: ${rawStringifiedInput}`, 38 | ); 39 | } 40 | 41 | if (!actorId) { 42 | throw new NodeOperationError(this.getNode(), 'Actor ID is required'); 43 | } 44 | 45 | // 1. Get the actor details 46 | const actor = await apiRequest.call(this, { 47 | method: 'GET', 48 | uri: `/v2/acts/${actorId}`, 49 | }); 50 | if (!actor || !actor.data) { 51 | throw new NodeApiError(this.getNode(), { 52 | message: `Actor ${actorId} not found`, 53 | }); 54 | } 55 | const actorData = actor.data; 56 | 57 | // 2. Build selection logic 58 | let build: any; 59 | if (buildParam) { 60 | build = await getBuildByTag.call(this, actorId, buildParam, actorData); 61 | } else { 62 | build = await getDefaultBuild.call(this, actorId); 63 | } 64 | 65 | // 3. Prepare query string 66 | const qs: Record = {}; 67 | if (timeout != null) qs.timeout = timeout; 68 | if (memory != null) qs.memory = memory; 69 | if (build?.buildNumber) qs.build = build.buildNumber; 70 | qs.waitForFinish = 0; // set initial run actor to not wait for finish 71 | 72 | // 4. Run the actor 73 | const run = await runActorApi.call(this, actorId, userInput, qs); 74 | if (!run?.data?.id) { 75 | throw new NodeApiError(this.getNode(), { 76 | message: `Run ID not found after running the actor`, 77 | }); 78 | } 79 | 80 | const runId = run.data.id; 81 | let lastRunData = run.data; 82 | 83 | // 5. If waitForFinish is true, poll for run status until it reaches a terminal state 84 | if (waitForFinish) { 85 | lastRunData = await pollRunStatus.call(this, runId); 86 | } 87 | 88 | return { 89 | runId, 90 | lastRunData, 91 | }; 92 | } 93 | 94 | export async function getBuildByTag( 95 | this: IExecuteFunctions, 96 | actorId: string, 97 | buildTag: string, 98 | actorData: any, 99 | ) { 100 | const buildByTag = actorData.taggedBuilds && actorData.taggedBuilds[buildTag]; 101 | if (!buildByTag?.buildId) { 102 | throw new NodeApiError(this.getNode(), { 103 | message: `Build tag '${buildTag}' does not exist for actor ${actorData.title ?? actorData.name ?? actorId}`, 104 | }); 105 | } 106 | const buildResp = await apiRequest.call(this, { 107 | method: 'GET', 108 | uri: `/v2/actor-builds/${buildByTag.buildId}`, 109 | }); 110 | if (!buildResp || !buildResp.data) { 111 | throw new NodeApiError(this.getNode(), { 112 | message: `Build with ID '${buildByTag.buildId}' not found for actor ${actorId}`, 113 | }); 114 | } 115 | return buildResp.data; 116 | } 117 | 118 | export async function getDefaultBuild(this: IExecuteFunctions, actorId: string) { 119 | const defaultBuildResp = await apiRequest.call(this, { 120 | method: 'GET', 121 | uri: `/v2/acts/${actorId}/builds/default`, 122 | }); 123 | if (!defaultBuildResp || !defaultBuildResp.data) { 124 | throw new NodeApiError(this.getNode(), { 125 | message: `Could not fetch default build for actor ${actorId}`, 126 | }); 127 | } 128 | return defaultBuildResp.data; 129 | } 130 | 131 | export async function runActorApi( 132 | this: IExecuteFunctions, 133 | actorId: string, 134 | mergedInput: any, 135 | qs: any, 136 | ) { 137 | return await apiRequest.call(this, { 138 | method: 'POST', 139 | uri: `/v2/acts/${actorId}/runs`, 140 | body: mergedInput, 141 | qs, 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /nodes/Apify/resources/actorResourceLocator.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; 2 | import { apiRequestAllItems } from './genericFunctions'; 3 | 4 | const resourceLocatorProperty: INodeProperties = { 5 | displayName: 'Actor', 6 | name: 'actorId', 7 | type: 'resourceLocator', 8 | default: { mode: 'list', value: '' }, 9 | modes: [ 10 | { 11 | displayName: 'From list', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Choose...', 15 | typeOptions: { 16 | searchListMethod: 'listActors', 17 | searchFilterRequired: false, 18 | searchable: true, 19 | }, 20 | }, 21 | { 22 | displayName: 'By URL', 23 | name: 'url', 24 | type: 'string', 25 | placeholder: 'https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input', 26 | validation: [ 27 | { 28 | type: 'regex', 29 | properties: { 30 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input 31 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku 32 | regex: 'https://console.apify.com/actors/([a-zA-Z0-9]+).*', 33 | errorMessage: 'Not a valid Actor URL', 34 | }, 35 | }, 36 | ], 37 | extractValue: { 38 | type: 'regex', 39 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku/input -> AtBpiepuIUNs2k2ku 40 | // https://console.apify.com/actors/AtBpiepuIUNs2k2ku -> AtBpiepuIUNs2k2ku 41 | regex: `https://console.apify.com/actors/([a-zA-Z0-9]+).*`, 42 | }, 43 | }, 44 | { 45 | displayName: 'ID', 46 | name: 'id', 47 | type: 'string', 48 | validation: [ 49 | { 50 | type: 'regex', 51 | properties: { 52 | regex: '[a-zA-Z0-9]+', 53 | errorMessage: 'Not a valid Actor ID', 54 | }, 55 | }, 56 | ], 57 | placeholder: 'NVCnbrChXaPbhVs8bISltEhngFg', 58 | url: '=http:/console.apify.com/actors/{{ $value }}/input', 59 | }, 60 | ], 61 | }; 62 | 63 | const actorSourceProperty: INodeProperties = { 64 | displayName: 'Actor Source', 65 | name: 'actorSource', 66 | type: 'options', 67 | options: [ 68 | { 69 | name: 'Recently Used Actors', 70 | value: 'recentlyUsed', 71 | }, 72 | { 73 | name: 'Apify Store Actors', 74 | value: 'store', 75 | }, 76 | ], 77 | default: 'recentlyUsed', 78 | description: 'Choose whether to select from your recently used Actors or browse Apify Store', 79 | displayOptions: { show: { resource: ['actor'] } }, 80 | }; 81 | 82 | function mapProperty(property: INodeProperties): INodeProperties { 83 | return { 84 | ...property, 85 | ...resourceLocatorProperty, 86 | }; 87 | } 88 | 89 | function createActorSourceProperty(displayOptions: any): INodeProperties { 90 | return { 91 | ...actorSourceProperty, 92 | displayOptions, 93 | }; 94 | } 95 | 96 | export function overrideActorProperties(properties: INodeProperties[]): INodeProperties[] { 97 | const result: INodeProperties[] = []; 98 | 99 | for (const property of properties) { 100 | if (property.name === 'actorId') { 101 | result.push(createActorSourceProperty(property.displayOptions)); 102 | result.push(mapProperty(property)); 103 | } else { 104 | result.push(property); 105 | } 106 | } 107 | 108 | return result; 109 | } 110 | 111 | export async function listActors( 112 | this: ILoadOptionsFunctions, 113 | searchTerm?: string, 114 | ): Promise { 115 | const actorSource = this.getNodeParameter('actorSource', 'recentlyUsed') as string; 116 | 117 | const mapToN8nSelectOption = (actor: any) => { 118 | const optionName = actor.title 119 | ? `${actor.title} (${actor.username}/${actor.name})` 120 | : `${actor.username}/${actor.name}`; 121 | 122 | return { 123 | name: optionName, 124 | value: actor.id, 125 | url: `https://console.apify.com/actors/${actor.id}/input`, 126 | description: actor.description || actor.name, 127 | }; 128 | }; 129 | 130 | const { 131 | data: { items: recentActors }, 132 | } = await apiRequestAllItems.call(this, { 133 | method: 'GET', 134 | uri: '/v2/acts', 135 | qs: { 136 | limit: 1000, 137 | offset: 0, 138 | sortBy: 'stats.lastRunStartedAt', 139 | desc: true, 140 | }, 141 | }); 142 | 143 | if (actorSource === 'recentlyUsed') { 144 | if (searchTerm) { 145 | const regex = new RegExp(searchTerm, 'i'); 146 | const filteredActors = recentActors.filter( 147 | (actor: any) => regex.test(actor.title || '') || regex.test(actor.name || ''), 148 | ); 149 | return { 150 | results: filteredActors.map(mapToN8nSelectOption), 151 | }; 152 | } 153 | return { 154 | results: recentActors.map(mapToN8nSelectOption), 155 | }; 156 | } 157 | 158 | const { 159 | data: { items: storeActors }, 160 | } = await apiRequestAllItems.call(this, { 161 | method: 'GET', 162 | uri: '/v2/store', 163 | qs: { 164 | limit: 200, 165 | offset: 0, 166 | search: searchTerm, 167 | }, 168 | }); 169 | 170 | return { 171 | results: storeActors.map(mapToN8nSelectOption), 172 | }; 173 | } 174 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/ApifyTrigger.node.spec.ts: -------------------------------------------------------------------------------- 1 | import apifyTriggerWorkflow from './workflows/webhook/webhook.workflow.json'; 2 | import { executeWorkflow } from './utils/executeWorkflow'; 3 | import { CredentialsHelper } from './utils/credentialHelper'; 4 | import * as fixtures from './utils/fixtures'; 5 | import * as genericFunctions from '../resources/genericFunctions'; 6 | import { Workflow } from 'n8n-workflow'; 7 | 8 | describe('Apify Trigger Node', () => { 9 | let credentialsHelper: CredentialsHelper; 10 | 11 | beforeAll(() => { 12 | credentialsHelper = new CredentialsHelper({ 13 | apifyApi: { 14 | apiToken: 'test-token', 15 | baseUrl: 'https://api.apify.com', 16 | }, 17 | }); 18 | }); 19 | 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | }); 23 | 24 | describe('checkExists', () => { 25 | const mockedCheckExists = async (workflow: Workflow) => 26 | await (workflow.nodeTypes as any).nodeTypes[ 27 | 'n8n-nodes-apify.apifyTrigger' 28 | ].type.webhookMethods.default.checkExists.call({ 29 | getNodeWebhookUrl: () => 30 | 'http://localhost:5678/2726981e-4e01-461f-a548-1f467e997400/webhook', 31 | getNodeParameter: (parameterName: string) => { 32 | switch (parameterName) { 33 | case 'resource': 34 | return 'actor'; 35 | case 'actorId': 36 | return { 37 | __rl: true, 38 | value: 'nFJndFXA5zjCTuudP', 39 | mode: 'list', 40 | cachedResultName: 'Google Search Results Scraper (apify/google-search-scraper)', 41 | cachedResultUrl: 'https://console.apify.com/actors/nFJndFXA5zjCTuudP/input', 42 | }; 43 | case 'eventType': 44 | return [ 45 | 'ACTOR.RUN.SUCCEEDED', 46 | 'ACTOR.RUN.ABORTED', 47 | 'ACTOR.RUN.FAILED', 48 | 'ACTOR.RUN.TIMED_OUT', 49 | ]; 50 | default: 51 | return ''; 52 | } 53 | }, 54 | }); 55 | 56 | it('should return false in checkExists webhook method since there is any webhook created', async () => { 57 | jest.spyOn(genericFunctions.apiRequest as any, 'call').mockResolvedValue({ 58 | // Spy and mock apiRequest.call to return an empty array (actor has no webhooks yet) 59 | data: { items: [] }, 60 | }); 61 | 62 | const { workflow } = await executeWorkflow({ 63 | credentialsHelper, 64 | workflow: apifyTriggerWorkflow, 65 | }); 66 | 67 | const result = await mockedCheckExists(workflow); 68 | expect(result).toEqual(false); 69 | }); 70 | 71 | it('should return true in checkExists webhook method since there is a webhook created with the same url', async () => { 72 | jest.spyOn(genericFunctions.apiRequest as any, 'call').mockResolvedValue({ 73 | // Spy and mock apiRequest.call to return a webhook with the same url (actor has webhooks) 74 | data: { items: fixtures.getActorWebhookResult().data.items }, 75 | }); 76 | 77 | const { workflow } = await executeWorkflow({ 78 | credentialsHelper, 79 | workflow: apifyTriggerWorkflow, 80 | }); 81 | 82 | const result = await mockedCheckExists(workflow); 83 | expect(result).toBe(true); 84 | }); 85 | }); 86 | 87 | describe('create', () => { 88 | const mockedCreate = async (workflow: Workflow) => 89 | await (workflow.nodeTypes as any).nodeTypes[ 90 | 'n8n-nodes-apify.apifyTrigger' 91 | ].type.webhookMethods.default.create.call({ 92 | getNodeWebhookUrl: () => 93 | 'http://localhost:5678/2726981e-4e01-461f-a548-1f467e997400/webhook', 94 | getNodeParameter: (parameterName: string) => { 95 | switch (parameterName) { 96 | case 'resource': 97 | return 'actor'; 98 | case 'actorId': 99 | return { 100 | __rl: true, 101 | value: 'nFJndFXA5zjCTuudP', 102 | mode: 'list', 103 | cachedResultName: 'Google Search Results Scraper (apify/google-search-scraper)', 104 | cachedResultUrl: 'https://console.apify.com/actors/nFJndFXA5zjCTuudP/input', 105 | }; 106 | case 'eventType': 107 | return [ 108 | 'ACTOR.RUN.SUCCEEDED', 109 | 'ACTOR.RUN.ABORTED', 110 | 'ACTOR.RUN.FAILED', 111 | 'ACTOR.RUN.TIMED_OUT', 112 | ]; 113 | default: 114 | return ''; 115 | } 116 | }, 117 | getWorkflowStaticData: () => { 118 | return workflow.staticData; 119 | }, 120 | }); 121 | 122 | it('should create the webhook', async () => { 123 | jest.spyOn(genericFunctions.apiRequest as any, 'call').mockResolvedValue({ 124 | data: fixtures.getCreateWebhookResult().data, 125 | }); 126 | 127 | const { workflow } = await executeWorkflow({ 128 | credentialsHelper, 129 | workflow: apifyTriggerWorkflow, 130 | }); 131 | 132 | const result = await mockedCreate(workflow); 133 | expect(result).toBe(true); 134 | 135 | const webhookData = workflow.staticData; 136 | expect(webhookData.webhookId).toEqual(fixtures.getCreateWebhookResult().data.id); 137 | }); 138 | }); 139 | 140 | describe('delete', () => { 141 | const mockedDelete = async (workflow: Workflow) => 142 | await (workflow.nodeTypes as any).nodeTypes[ 143 | 'n8n-nodes-apify.apifyTrigger' 144 | ].type.webhookMethods.default.delete.call({ 145 | getWorkflowStaticData: () => { 146 | return workflow.staticData; 147 | }, 148 | }); 149 | it('should delete the webhook', async () => { 150 | const webhookId = fixtures.getCreateWebhookResult().data.id; 151 | 152 | jest.spyOn(genericFunctions.apiRequest as any, 'call').mockResolvedValue(null); 153 | 154 | const { workflow } = await executeWorkflow({ 155 | credentialsHelper, 156 | workflow: apifyTriggerWorkflow, 157 | }); 158 | 159 | const node = Object.values(workflow.nodes)[0]; 160 | const webhookData = workflow.staticData; 161 | webhookData.webhookId = webhookId; 162 | 163 | const result = await mockedDelete(workflow); 164 | expect(result).toBe(true); 165 | 166 | expect(workflow.getStaticData('node', node)).toEqual({}); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /nodes/Apify/ApifyTrigger.node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n8n-nodes-base/node-class-description-outputs-wrong */ 2 | 3 | import { 4 | IDataObject, 5 | IHookFunctions, 6 | INodeType, 7 | INodeTypeDescription, 8 | IWebhookFunctions, 9 | IWebhookResponseData, 10 | NodeConnectionType, 11 | } from 'n8n-workflow'; 12 | import { 13 | apiRequest, 14 | compose, 15 | generateIdempotencyKey, 16 | getActorOrTaskId, 17 | getCondition, 18 | normalizeEventTypes, 19 | } from './resources/genericFunctions'; 20 | import { listActors, overrideActorProperties } from './resources/actorResourceLocator'; 21 | import { listActorTasks, overrideActorTaskProperties } from './resources/actorTaskResourceLocator'; 22 | 23 | const triggerProperties = compose(overrideActorProperties, overrideActorTaskProperties); 24 | 25 | export class ApifyTrigger implements INodeType { 26 | description: INodeTypeDescription = { 27 | displayName: 'Apify Trigger', 28 | name: 'apifyTrigger', 29 | icon: 'file:apify.svg', 30 | group: ['trigger'], 31 | version: 1, 32 | description: 'Triggers workflow on Apify Actor or task run events', 33 | defaults: { name: 'Apify Trigger' }, 34 | inputs: [], 35 | outputs: [NodeConnectionType.Main], 36 | credentials: [ 37 | { 38 | displayName: 'Apify API key connection', 39 | name: 'apifyApi', 40 | required: false, 41 | displayOptions: { 42 | show: { 43 | authentication: ['apifyApi'], 44 | }, 45 | }, 46 | }, 47 | { 48 | displayName: 'Apify OAuth2 connection', 49 | name: 'apifyOAuth2Api', 50 | required: false, 51 | displayOptions: { 52 | show: { 53 | authentication: ['apifyOAuth2Api'], 54 | }, 55 | }, 56 | }, 57 | ], 58 | webhooks: [ 59 | { 60 | name: 'default', 61 | httpMethod: 'POST', 62 | responseMode: 'onReceived', 63 | path: 'webhook', 64 | }, 65 | ], 66 | properties: triggerProperties([ 67 | { 68 | displayName: 'Authentication', 69 | name: 'authentication', 70 | type: 'options', 71 | options: [ 72 | { 73 | name: 'API Key', 74 | value: 'apifyApi', 75 | }, 76 | { 77 | name: 'OAuth2', 78 | value: 'apifyOAuth2Api', 79 | }, 80 | ], 81 | default: 'apifyApi', 82 | description: 'Choose which authentication method to use', 83 | }, 84 | { 85 | displayName: 'Resource to Watch', 86 | name: 'resource', 87 | type: 'options', 88 | noDataExpression: true, 89 | options: [ 90 | { name: 'Actor', value: 'actor' }, 91 | { name: 'Task', value: 'task' }, 92 | ], 93 | default: 'actor', 94 | description: 'Whether to trigger when an Actor or a task run finishes', 95 | }, 96 | { 97 | displayName: 'Actor Source', 98 | name: 'actorSource', 99 | type: 'hidden', 100 | displayOptions: { show: { resource: ['actor'] } }, 101 | default: 'recentlyUsed', 102 | }, 103 | { 104 | displayName: 'Actor', 105 | name: 'actorId', 106 | required: true, 107 | description: 'Actor ID or a tilde-separated username and Actor name', 108 | default: 'janedoe~my-actor', 109 | type: 'string', 110 | displayOptions: { show: { resource: ['actor'] } }, 111 | }, 112 | { 113 | displayName: 'Saved Tasks Name or ID', 114 | name: 'actorTaskId', 115 | type: 'string', 116 | default: '', 117 | description: 118 | 'Apify task to monitor for runs. Choose from the list, or specify an ID using an expression.', 119 | displayOptions: { show: { resource: ['task'] } }, 120 | placeholder: 'Select task to watch', 121 | }, 122 | { 123 | displayName: 'Event Type', 124 | name: 'eventType', 125 | type: 'multiOptions', 126 | options: [ 127 | { 128 | name: 'Aborted', 129 | value: 'ACTOR.RUN.ABORTED', 130 | description: 'Trigger when Actor or task run is aborted', 131 | }, 132 | { name: 'Any', value: 'any', description: 'Trigger on any terminal event' }, 133 | { 134 | name: 'Failed', 135 | value: 'ACTOR.RUN.FAILED', 136 | description: 'Trigger when Actor or task run fails', 137 | }, 138 | { 139 | name: 'Succeeded', 140 | value: 'ACTOR.RUN.SUCCEEDED', 141 | description: 'Trigger when Actor or task run completes successfully', 142 | }, 143 | { 144 | name: 'Timed Out', 145 | value: 'ACTOR.RUN.TIMED_OUT', 146 | description: 'Trigger when Actor or task run times out', 147 | }, 148 | ], 149 | default: ['ACTOR.RUN.SUCCEEDED'], 150 | description: 'The status of the Actor or task run that should trigger the workflow', 151 | }, 152 | ]), 153 | }; 154 | 155 | webhookMethods = { 156 | default: { 157 | async checkExists(this: IHookFunctions): Promise { 158 | const webhookUrl = this.getNodeWebhookUrl('default') as string; 159 | const actorOrTaskId = getActorOrTaskId.call(this); 160 | 161 | if (!actorOrTaskId) { 162 | return false; 163 | } 164 | 165 | const { 166 | data: { items: webhooks }, 167 | } = await apiRequest.call(this, { method: 'GET', uri: '/v2/webhooks' }); 168 | 169 | return webhooks.some( 170 | (webhook: any) => 171 | webhook.requestUrl === webhookUrl && 172 | (webhook.condition.actorId === actorOrTaskId || 173 | webhook.condition.actorTaskId === actorOrTaskId), 174 | ); 175 | }, 176 | 177 | async create(this: IHookFunctions): Promise { 178 | const webhookUrl = this.getNodeWebhookUrl('default') as string; 179 | const resource = this.getNodeParameter('resource') as string; 180 | const selectedEventTypes = this.getNodeParameter('eventType', []) as string[]; 181 | const actorOrTaskId = getActorOrTaskId.call(this); 182 | const webhookData = this.getWorkflowStaticData('node'); 183 | 184 | if (!actorOrTaskId) { 185 | return false; 186 | } 187 | 188 | const condition = getCondition.call(this, resource, actorOrTaskId); 189 | const idempotencyKey = generateIdempotencyKey.call( 190 | this, 191 | resource, 192 | actorOrTaskId, 193 | selectedEventTypes, 194 | ); 195 | const eventTypes = normalizeEventTypes.call(this, selectedEventTypes); 196 | 197 | const body: IDataObject = { 198 | eventTypes: eventTypes, 199 | requestUrl: webhookUrl, 200 | condition, 201 | idempotencyKey, 202 | }; 203 | 204 | const { 205 | data: { id }, 206 | } = await apiRequest.call(this, { method: 'POST', uri: '/v2/webhooks', body }); 207 | webhookData.webhookId = id; 208 | return true; 209 | }, 210 | 211 | async delete(this: IHookFunctions): Promise { 212 | const webhookData = this.getWorkflowStaticData('node'); 213 | if (!webhookData.webhookId) return false; 214 | 215 | await apiRequest.call(this, { 216 | method: 'DELETE', 217 | uri: `/v2/webhooks/${webhookData.webhookId}`, 218 | }); 219 | delete webhookData.webhookId; 220 | return true; 221 | }, 222 | }, 223 | }; 224 | 225 | async webhook(this: IWebhookFunctions): Promise { 226 | const req = this.getRequestObject(); 227 | return { 228 | workflowData: [this.helpers.returnJsonArray(req.body as IDataObject)], 229 | }; 230 | } 231 | 232 | methods = { 233 | listSearch: { 234 | listActors, 235 | listActorTasks, 236 | }, 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /nodes/Apify/resources/genericFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sleep, 3 | NodeApiError, 4 | NodeOperationError, 5 | type IDataObject, 6 | type IExecuteFunctions, 7 | type IHookFunctions, 8 | type ILoadOptionsFunctions, 9 | type IRequestOptions, 10 | } from 'n8n-workflow'; 11 | import { 12 | DEFAULT_EXP_BACKOFF_EXPONENTIAL, 13 | DEFAULT_EXP_BACKOFF_INTERVAL, 14 | DEFAULT_EXP_BACKOFF_RETRIES, 15 | } from '../helpers/consts'; 16 | 17 | type IApiRequestOptions = IRequestOptions & { uri?: string }; 18 | 19 | /** 20 | * Make an API request to Apify 21 | */ 22 | export async function apiRequest( 23 | this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 24 | requestOptions: IApiRequestOptions, 25 | ): Promise { 26 | const { method, qs, uri, ...rest } = requestOptions; 27 | 28 | const query = qs || {}; 29 | const endpoint = `https://api.apify.com${uri}`; 30 | 31 | const headers: Record = { 32 | 'x-apify-integration-platform': 'n8n', 33 | }; 34 | 35 | if (isUsedAsAiTool(this.getNode().type)) { 36 | headers['x-apify-integration-ai-tool'] = 'true'; 37 | } 38 | 39 | const options: IRequestOptions = { 40 | json: true, 41 | ...rest, 42 | method, 43 | qs: query, 44 | url: endpoint, 45 | headers, 46 | }; 47 | 48 | if (method === 'GET') { 49 | delete options.body; 50 | } 51 | 52 | try { 53 | const authenticationMethod = this.getNodeParameter('authentication', 0) as string; 54 | try { 55 | await this.getCredentials(authenticationMethod); 56 | } catch { 57 | throw new NodeOperationError( 58 | this.getNode(), 59 | `No valid credentials found for ${authenticationMethod}. Please configure them first.`, 60 | ); 61 | } 62 | 63 | return await retryWithExponentialBackoff(() => 64 | this.helpers.requestWithAuthentication.call(this, authenticationMethod, options), 65 | ); 66 | } catch (error) { 67 | /** 68 | * using `error instanceof NodeApiError` results in `false` 69 | * because it's thrown by a different instance of n8n-workflow 70 | */ 71 | if (error.constructor?.name === 'NodeApiError') { 72 | throw error; 73 | } 74 | 75 | if (error.response && error.response.body) { 76 | throw new NodeApiError(this.getNode(), error, { 77 | message: error.response.body, 78 | description: error.message, 79 | }); 80 | } 81 | 82 | throw new NodeApiError(this.getNode(), error); 83 | } 84 | } 85 | 86 | /** 87 | * Checks if the given status code is retryable 88 | * Status codes 429 (rate limit) and 500+ are retried, 89 | * Other status codes 300-499 (except 429) are not retried, 90 | * because the error is probably caused by invalid URL (redirect 3xx) or invalid user input (4xx). 91 | */ 92 | function isStatusCodeRetryable(statusCode: number) { 93 | if (Number.isNaN(statusCode)) return false; 94 | 95 | const RATE_LIMIT_EXCEEDED_STATUS_CODE = 429; 96 | const isRateLimitError = statusCode === RATE_LIMIT_EXCEEDED_STATUS_CODE; 97 | const isInternalError = statusCode >= 500; 98 | return isRateLimitError || isInternalError; 99 | } 100 | 101 | /** 102 | * Wraps a function with exponential backoff. 103 | * If request fails with http code 500+ or doesn't return 104 | * a code at all it is retried in 1s,2s,4s,.. up to maxRetries 105 | * @param fn 106 | * @param interval 107 | * @param exponential 108 | * @param maxRetries 109 | * @returns 110 | */ 111 | export async function retryWithExponentialBackoff( 112 | fn: () => Promise, 113 | interval: number = DEFAULT_EXP_BACKOFF_INTERVAL, 114 | exponential: number = DEFAULT_EXP_BACKOFF_EXPONENTIAL, 115 | maxRetries: number = DEFAULT_EXP_BACKOFF_RETRIES, 116 | ): Promise { 117 | let lastError; 118 | for (let i = 0; i < maxRetries; i++) { 119 | try { 120 | return await fn(); 121 | } catch (error) { 122 | lastError = error; 123 | const status = Number(error?.httpCode); 124 | if (isStatusCodeRetryable(status)) { 125 | //Generate a new sleep time based from interval * exponential^i function 126 | const sleepTimeSecs = interval * Math.pow(exponential, i); 127 | const sleepTimeMs = sleepTimeSecs * 1000; 128 | 129 | await sleep(sleepTimeMs); 130 | 131 | continue; 132 | } 133 | throw error; 134 | } 135 | } 136 | //In case all of the calls failed with no status or isStatusCodeRetryable, throw the last error 137 | throw lastError; 138 | } 139 | 140 | export async function apiRequestAllItems( 141 | this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 142 | requestOptions: IApiRequestOptions, 143 | ): Promise { 144 | const returnData: IDataObject[] = []; 145 | if (!requestOptions.qs) requestOptions.qs = {}; 146 | requestOptions.qs.limit = requestOptions.qs.limit || 999; 147 | 148 | let responseData; 149 | 150 | do { 151 | responseData = await apiRequest.call(this, requestOptions); 152 | returnData.push(responseData); 153 | } while (requestOptions.qs.limit <= responseData.length); 154 | 155 | const combinedData = { 156 | data: { 157 | total: 0, 158 | count: 0, 159 | offset: 0, 160 | limit: 0, 161 | desc: false, 162 | items: [] as IDataObject[], 163 | }, 164 | }; 165 | 166 | for (const result of returnData) { 167 | combinedData.data.total += typeof result.total === 'number' ? result.total : 0; 168 | combinedData.data.count += typeof result.count === 'number' ? result.count : 0; 169 | combinedData.data.offset += typeof result.offset === 'number' ? result.offset : 0; 170 | combinedData.data.limit += typeof result.limit === 'number' ? result.limit : 0; 171 | 172 | if ( 173 | result.data && 174 | typeof result.data === 'object' && 175 | 'items' in result.data && 176 | Array.isArray((result.data as IDataObject).items) 177 | ) { 178 | combinedData.data.items = [ 179 | ...combinedData.data.items, 180 | ...(result.data.items as IDataObject[]), 181 | ]; 182 | } 183 | } 184 | 185 | return combinedData; 186 | } 187 | 188 | export async function pollRunStatus( 189 | this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 190 | runId: string, 191 | ): Promise { 192 | let lastRunData: any; 193 | while (true) { 194 | try { 195 | const pollResult = await apiRequest.call(this, { 196 | method: 'GET', 197 | uri: `/v2/actor-runs/${runId}`, 198 | }); 199 | const status = pollResult?.data?.status; 200 | lastRunData = pollResult?.data; 201 | if (['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'].includes(status)) { 202 | break; 203 | } 204 | } catch (err) { 205 | throw new NodeApiError(this.getNode(), { 206 | message: `Error polling run status: ${err}`, 207 | }); 208 | } 209 | await sleep(1000); // 1 second polling interval 210 | } 211 | return lastRunData; 212 | } 213 | 214 | export function getActorOrTaskId(this: IHookFunctions): string { 215 | const resource = this.getNodeParameter('resource', '') as string; 216 | const actorId = this.getNodeParameter('actorId', '') as { value: string }; 217 | const actorTaskId = this.getNodeParameter('actorTaskId', '') as { value: string }; 218 | 219 | if (resource === 'task') { 220 | return actorTaskId.value; 221 | } 222 | 223 | return actorId.value; 224 | } 225 | 226 | export function getCondition(this: IHookFunctions, resource: string, id: string): object { 227 | return resource === 'actor' ? { actorId: id } : { actorTaskId: id }; 228 | } 229 | 230 | export function normalizeEventTypes(selected: string[]): string[] { 231 | if (selected.includes('any')) { 232 | return ['ACTOR.RUN.SUCCEEDED', 'ACTOR.RUN.FAILED', 'ACTOR.RUN.TIMED_OUT', 'ACTOR.RUN.ABORTED']; 233 | } 234 | return selected; 235 | } 236 | 237 | export function generateIdempotencyKey( 238 | resource: string, 239 | actorOrTaskId: string, 240 | eventTypes: string[], 241 | ): string { 242 | const sortedEventTypes = [...eventTypes].sort(); 243 | const raw = `${resource}:${actorOrTaskId}:${sortedEventTypes.join(',')}`; 244 | return Buffer.from(raw).toString('base64'); 245 | } 246 | 247 | export function compose(...fns: Function[]) { 248 | return (x: any) => fns.reduce((v, f) => f(v), x); 249 | } 250 | 251 | export function customBodyParser(input: string | object) { 252 | if (!input) { 253 | return {}; 254 | } 255 | 256 | if (typeof input === 'string') { 257 | return input ? JSON.parse(input) : {}; 258 | } else { 259 | // When an AI Agent Tool calls the node 260 | // It sometimes sends an object instead of a string 261 | return input; 262 | } 263 | } 264 | 265 | export function isUsedAsAiTool(nodeType: string): boolean { 266 | const parts = nodeType.split('.'); 267 | return parts[parts.length - 1] === 'apifyTool'; 268 | } 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n Nodes - Apify integration 2 | 3 | This is an n8n community node that integrates [Apify](https://apify.com) with your n8n workflows, so you can run Apify Actors, extract structured data from websites, and automate complex web scraping tasks. 4 | 5 | [Apify](https://apify.com) is a platform for developers to build, deploy, and publish web automation tools, while [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) tool for AI workflow automation that allows you to connect various services. 6 | 7 | ## Table of contents 8 | 9 | - [Installation on self hosted instance](#installation-self-hosted) 10 | - [Installation on n8n cloud](#installation-n8n-cloud) 11 | - [Installation for development and contributing](#installation-development-and-contributing) 12 | - [Operations](#operations) 13 | - [Credentials](#credentials) 14 | - [Compatibility](#compatibility) 15 | - [Usage](#usage) 16 | - [Resources](#resources) 17 | - [Release](#releasing-a-new-version) 18 | - [Version History](#version-history) 19 | - [Troubleshooting](#troubleshooting) 20 | 21 | ## Installation (self-hosted) 22 | 23 | To install the Apify community node directly from the n8n Editor UI: 24 | 25 | 1. Open your n8n instance. 26 | 2. Go to **Settings > Community Nodes** 27 | 3. Select **Install**. 28 | 4. Enter the npm package name: `@apify/n8n-nodes-apify` to install the latest version. To install a specific version (e.g 0.4.4) enter `@apify/n8n-nodes-apify@0.4.4`. All versions are available [here](https://www.npmjs.com/package/@apify/n8n-nodes-apify?activeTab=versions) 29 | 5. Agree to the [risks](https://docs.n8n.io/integrations/community-nodes/risks/) of using community nodes and select **Install** 30 | 6. The node is now available to use in your workflows. 31 | 32 | ## Installation (n8n Cloud) 33 | 34 | If you’re using n8n Cloud, installing community nodes is even simpler: 35 | 36 | 1. Go to the **Canvas** and open the **nodes panel**. 37 | 2. Search for **Apify** in the community node registry. 38 | 3. Click **Install node** to add the Apify node to your instance. 39 | 40 | 41 | > On n8n cloud users can choose not to show verified community nodes. Instance owners can toggle this in the Cloud Admin Panel. To install the Apify node, make sure the installation of verified community nodes is enabled. 42 | 43 | 44 | ## Installation (development and contributing) 45 | To contribute to our Apify node, you can install the node and link it to your n8n instance. Follow the steps below: 46 | 47 | ### ⚙️ Prerequisites 48 | 49 | - Node.js (recommended: v18.10+) 50 | - pnpm installed globally 51 | 52 | --- 53 | 54 | ### 1. Initialize n8n locally 55 | 56 | Begin by installing and running n8n to create the necessary configuration directory (`~/.n8n`): 57 | 58 | ```bash 59 | npm install -g n8n # Skip this step if you already have n8n installed globally 60 | n8n start # This will generate the ~/.n8n directory 61 | ``` 62 | 63 | ### 2. Clone and build the Node Package 64 | 65 | Install dependencies and build the node: 66 | 67 | ```bash 68 | pnpm install 69 | pnpm run build 70 | ``` 71 | 72 | ### 3. Link the custom node to n8n 73 | 74 | Create the `custom` directory inside `~/.n8n` (if it doesn't exist), then symlink your local node package: 75 | 76 | ```bash 77 | mkdir -p ~/.n8n/custom 78 | ln -s /full/path/to/n8n-nodes-apify ~/.n8n/custom/n8n-nodes-apify # replace full/path/to with the path to your n8n-nodes-apify directory 79 | ``` 80 | 81 | > **Note:** Use the absolute path in the symlink for compatibility. 82 | 83 | ### 4. Restart n8n 84 | 85 | Now that your custom node is linked, start n8n again: 86 | 87 | ```bash 88 | n8n start 89 | ``` 90 | 91 | --- 92 | 93 | ### 🔁 Making changes 94 | 95 | If you make any changes to your custom node locally, remember to rebuild and restart: 96 | 97 | ```bash 98 | pnpm run build 99 | n8n start 100 | ``` 101 | 102 | --- 103 | 104 | ## Self-hosted n8n: Public webhook URL for triggers 105 | 106 | This configuration is required for our service's trigger functionality to work correctly. 107 | 108 | By default, when running locally n8n generates webhook URLs using `localhost`, which external services cannot reach. To fix this: 109 | 110 | 1. **Set your webhook URL** 111 | In the same shell or Docker environment where n8n runs, export the `WEBHOOK_URL` to a publicly-accessible address. For example: 112 | ```bash 113 | export WEBHOOK_URL="https://your-tunnel.local" 114 | ``` 115 | 2. **Restart n8n** 116 | ```bash 117 | pnpm run build 118 | n8n start 119 | ``` 120 | 121 | ## Operations 122 | 123 | ![operations](./docs/actions.png) 124 | 125 | This node supports a wide range of Apify operations, organized by resource type: 126 | 127 | ### Actors 128 | - **Run Actor**: Execute an Actor with optional input parameters 129 | - **Default behavior**: Uses predefined input values 130 | - **Custom input**: Provide JSON object to override any or all default parameters. 131 | - Configurable timeout and memory limits 132 | - Build version selection 133 | - **Run Actor and get dataset items**: Execute an Actor, waits for it to finish, and finally returns the dataset items 134 | - **Scrape Single URL**: Quick scraping of a single URL 135 | - **Get Last Run**: Retrieve information about the most recent Actor run 136 | 137 | ![actor run](./docs/run-actor.png) 138 | 139 | ### Actor tasks 140 | - **Run Task**: Execute a predefined Actor task 141 | - Supports custom input JSON 142 | - Configurable timeout 143 | - Task-specific settings 144 | - **Run task and get dataset items**: Execute a task, wait for it to finish, and return the dataset items 145 | 146 | ### Actor runs 147 | - **Get User Runs List**: List all runs for a user 148 | - Pagination support 149 | - Sorting options 150 | - Status filtering 151 | - **Get run**: Retrieve detailed information about a specific run 152 | 153 | ### Datasets 154 | - **Get Items**: Fetch items from a dataset 155 | 156 | ### Key-Value Stores 157 | - **Get Key-Value Store Record**: Retrieve a specific record by key 158 | 159 | ### Triggers 160 | Automatically start an n8n workflow whenever an Actor or task finishes execution 161 | - Can be configured to trigger on success, failure, abort, timeout or any combination of these states 162 | - Includes run metadata in the output 163 | - Available triggers: 164 | - **Actor Run Finished**: Start a workflow when an Actor run completes 165 | - **Task Run Finished**: Start a workflow when a task run completes 166 | 167 | ![triggers](./docs/trigger.png) 168 | 169 | ### AI Tools 170 | 171 | All Apify node operations can be combined with n8n's AI tools to create powerful workflows. 172 | For example, you can scrape data from a website using an Actor and then use an AI model to analyze or summarize the extracted information. 173 | 174 | ## Credentials 175 | 176 | The node supports two authentication methods: 177 | 178 | 1. **API key authentication** 179 | - Configure your Apify API key in the n8n credentials section under `apifyApi` 180 | 181 | 2. **OAuth2 authentication** (available only in n8n cloud) 182 | - Configure OAuth2 credentials in the n8n credentials section under `apifyOAuth2Api` 183 | 184 | ![auth](./docs/auth.png) 185 | 186 | 187 | ## Compatibility 188 | 189 | This node has been tested with n8n version 1.57.0. 190 | 191 | ## Usage 192 | 193 | 1. **Create an Actor**: Set up a new Actor on [Apify](https://apify.com). 194 | 2. **Set up a workflow**: Create a new workflow in n8n. 195 | 3. **Add the Apify node**: Insert the Apify node into your workflow. 196 | 4. **Configure credentials**: Enter your Apify API key and Actor ID. 197 | 5. **Select an operation**: Choose the desired operation for the node. 198 | 6. **Execute the workflow**: Run the workflow to execute the Apify operation. 199 | 200 | ![workflow](./docs/workflow.png) 201 | 202 | ## Resources 203 | 204 | - [n8n Community Nodes Documentation](https://docs.n8n.io/integrations/community-nodes/) 205 | - [Apify API Documentation](https://docs.apify.com) 206 | 207 | # Releasing a New Version 208 | 209 | This project uses a GitHub Actions workflow to automate the release process, including publishing to npm. Here's how to trigger a new release. 210 | 211 | **Prerequisites (for all methods):** 212 | 213 | * Ensure your target branch on GitHub is up-to-date with all changes you want to include in the release. 214 | * Decide on the new version number, following semantic versioning (e.g., `vX.Y.Z`). 215 | * Prepare your release notes detailing the changes. 216 | * If you're using CLI to release, make sure you have the [GitHub CLI (`gh`)](https://cli.github.com/) installed and authenticated (`gh auth login`). 217 | 218 | --- 219 | 220 | ## Method 1: Using the GitHub Web UI (Recommended for ease of use) 221 | 222 | 1. **Navigate to GitHub Releases:** 223 | * Go to your repository’s "Releases" tab 224 | 225 | 2. **Draft a New Release:** 226 | * Click the **“Draft a new release”** button. 227 | 228 | 3. **Create or Choose a Tag:** 229 | * In the “Choose a tag” dropdown: 230 | * **Type your new tag name** (e.g., `v1.2.3`). 231 | * If the tag doesn't exist, GitHub will prompt you with an option like **“Create new tag: v1.2.3 on publish.”** Click this. 232 | * Ensure the **target branch** selected for creating the new tag is correct. This tag will point to the latest commit on this target branch. 233 | 234 | 4. **Set Release Title and Notes:** 235 | * Set the "Release title" (e.g., `vX.Y.Z` or a more descriptive title). 236 | * For the release notes in the description field, you have a few options: 237 | * **Write your prepared release notes.** 238 | * **Click the "Generate release notes" button:** GitHub will attempt to automatically create release notes based on merged pull requests since the last release. You can then review and edit these auto-generated notes. 239 | 240 | 5. **Publish the Release:** 241 | * Click the **“Publish release”** button. 242 | 243 | *Upon publishing, GitHub creates the tag from your specified branch and then creates the release. This "published" release event triggers the automated workflow.* 244 | 245 | --- 246 | 247 | ## Method 2: Fully CLI-Driven Release 248 | 249 | This method uses the GitHub CLI (`gh`) for all steps, including tag creation. 250 | 251 | 1. **Ensure your local target branch is synced and changes are pushed:** 252 | ```bash 253 | git checkout master 254 | git pull origin master 255 | ``` 256 | 257 | 2. **Create the Release (which also creates and pushes the tag):** 258 | Replace `vX.Y.Z` with your desired tag/version. The command will create this tag from the latest commit of your specified `--target` branch (defaults to repository's default branch, if `--target` is omitted and the branch is up to date). 259 | 260 | ```bash 261 | gh release create vX.Y.Z \ 262 | --target master \ 263 | --title "vX.Y.Z" \ 264 | --notes "Your detailed release notes here. 265 | - Feature X 266 | - Bugfix Y" 267 | 268 | # Or, to use notes from a file: 269 | gh release create vX.Y.Z \ 270 | --target master \ 271 | --title "vX.Y.Z" \ 272 | --notes-file ./RELEASE_NOTES.md 273 | 274 | # Or, to generate notes from pull requests (commits must follow conventional commit format for best results): 275 | gh release create vX.Y.Z \ 276 | --target master \ 277 | --title "vX.Y.Z" 278 | --generate-notes 279 | ``` 280 | 281 | * `vX.Y.Z`: The tag and release name. 282 | * `--target `: Specifies which branch the tag should be created from (e.g., `master`). If the tag `vX.Y.Z` doesn't exist, `gh` will create it based on the HEAD of this target branch and push it. 283 | * `--title ""`: The title for your release. 284 | * `--notes "<notes>"` or `--notes-file <filepath>` or `--generate-notes`: Your release notes. 285 | 286 | *This command will create the tag, push it to GitHub, and then publish the release. This "published" release event triggers the automated workflow.* 287 | 288 | --- 289 | 290 | ## Post-Release: Automated Workflow & Verification (Common to all methods) 291 | 292 | Regardless of how you create and publish the GitHub Release: 293 | 294 | 1. **Automated Workflow Execution:** 295 | * The "Release & Publish" GitHub Actions workflow will automatically trigger. 296 | * It will perform: 297 | 1. Code checkout. 298 | 2. Version extraction (`X.Y.Z`) from the release tag. 299 | 3. Build and test processes. 300 | 4. Update `package.json` and `pnpm-lock.yaml` to version `X.Y.Z`. 301 | 5. Commit these version changes back to the branch the release was targeted from with a message like `chore(release): set version to X.Y.Z [skip ci]`. 302 | 6. Publish the package `@apify/n8n-nodes-apify@X.Y.Z` to npm. 303 | 304 | 2. **Verify the Package on npm:** 305 | After the workflow successfully completes (check the "Actions" tab in your GitHub repository): 306 | * Verify the new version on npm: 307 | ```bash 308 | pnpm view @apify/n8n-nodes-apify version 309 | ``` 310 | This should print `X.Y.Z`. 311 | 312 | ## Version history 313 | 314 | Track changes and updates to the node here. 315 | 316 | ## Troubleshooting 317 | 318 | ### Common issues 319 | 320 | 1. **Authentication errors** 321 | - Verify your API key is correct 322 | 323 | 2. **Resource Not Found** 324 | - Verify the resource ID format 325 | - Check if the resource exists in your Apify account 326 | - Ensure you have access to the resource 327 | 328 | 3. **Operation failures** 329 | - Check the input parameters 330 | - Verify resource limits (memory, timeout) 331 | - Review Apify Console for detailed error messages 332 | 333 | ### Getting help 334 | 335 | If you encounter issues: 336 | 1. Check the [Apify API documentation](https://docs.apify.com) 337 | 2. Review the [n8n Community Nodes documentation](https://docs.n8n.io/integrations/community-nodes/) 338 | 3. Open an issue in the [GitHub repository](https://github.com/apify/n8n-nodes-apify) 339 | -------------------------------------------------------------------------------- /nodes/Apify/__tests__/Apify.node.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { Apify } from '../Apify.node'; 3 | import { executeWorkflow } from './utils/executeWorkflow'; 4 | import { CredentialsHelper } from './utils/credentialHelper'; 5 | import { getRunTaskDataByNodeName, getTaskData } from './utils/getNodeResultData'; 6 | import getRunWorkflow from './workflows/actor-runs/get-run.workflow.json'; 7 | import getUserRunsListWorkflow from './workflows/actor-runs/get-user-runs-list.workflow.json'; 8 | import getRunsWorkflow from './workflows/actor-runs/get-runs.workflow.json'; 9 | import runActorAndGetDatasetWorkflow from './workflows/actors/run-actor-and-get-dataset.workflow.json'; 10 | import runTaskAndGetDatasetWorkflow from './workflows/actor-tasks/run-task-and-get-dataset.workflow.json'; 11 | import * as fixtures from './utils/fixtures'; 12 | import * as helpers from '../helpers'; 13 | import { getTaskArrayData } from './utils/getNodeResultData'; 14 | 15 | describe('Apify Node', () => { 16 | let apifyNode: Apify; 17 | let credentialsHelper: CredentialsHelper; 18 | 19 | beforeEach(() => { 20 | apifyNode = new Apify(); 21 | credentialsHelper = new CredentialsHelper({ 22 | apifyApi: { 23 | apiToken: 'test-token', 24 | baseUrl: 'https://api.apify.com', 25 | }, 26 | }); 27 | }); 28 | 29 | describe('description', () => { 30 | it('should have a name property', () => { 31 | expect(apifyNode.description.name).toBeDefined(); 32 | expect(apifyNode.description.name).toEqual('apify'); 33 | }); 34 | 35 | it('should have properties defined', () => { 36 | expect(apifyNode.description.properties).toBeDefined(); 37 | }); 38 | 39 | it('should have credential properties defined', () => { 40 | expect(apifyNode.description.credentials).toBeDefined(); 41 | }); 42 | }); 43 | 44 | describe('actor-runs', () => { 45 | describe('get-run', () => { 46 | it('should run the get-run workflow', async () => { 47 | const runId = 'c7Orwz5b830Tbp784'; 48 | const mockRun = fixtures.getRunResult(); 49 | 50 | const scope = nock('https://api.apify.com') 51 | .get(`/v2/actor-runs/${runId}`) 52 | .reply(200, mockRun); 53 | 54 | const { executionData } = await executeWorkflow({ 55 | credentialsHelper, 56 | workflow: getRunWorkflow, 57 | }); 58 | 59 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get run'); 60 | expect(nodeResults.length).toBe(1); 61 | const [nodeResult] = nodeResults; 62 | expect(nodeResult.executionStatus).toBe('success'); 63 | 64 | const data = getTaskData(nodeResult); 65 | expect(data).toEqual(mockRun.data); 66 | 67 | expect(scope.isDone()).toBe(true); 68 | }); 69 | }); 70 | 71 | describe('get-runs', () => { 72 | it('should run the get-user-runs-list workflow', async () => { 73 | const mockRunsList = fixtures.getUserRunsListResult(); 74 | 75 | const scope = nock('https://api.apify.com') 76 | .get('/v2/actor-runs') 77 | .query(true) 78 | .reply(200, mockRunsList); 79 | 80 | const { executionData } = await executeWorkflow({ 81 | credentialsHelper, 82 | workflow: getUserRunsListWorkflow, 83 | }); 84 | 85 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get user runs list'); 86 | expect(nodeResults.length).toBe(1); 87 | const [nodeResult] = nodeResults; 88 | expect(nodeResult.executionStatus).toBe('success'); 89 | 90 | const data = getTaskArrayData(nodeResult); 91 | expect(Array.isArray(data)).toBe(true); 92 | expect(data?.map((item) => item.json)).toEqual(mockRunsList.data.items); 93 | 94 | expect(scope.isDone()).toBe(true); 95 | }); 96 | }); 97 | 98 | describe('get-actor-runs', () => { 99 | it('should run the get-actor-runs workflow', async () => { 100 | const mockRunsList = fixtures.getActorRunsResult(); 101 | 102 | const scope = nock('https://api.apify.com') 103 | .get('/v2/acts/nFJndFXA5zjCTuudP/runs') 104 | .query(true) 105 | .reply(200, mockRunsList); 106 | 107 | const { executionData } = await executeWorkflow({ 108 | credentialsHelper, 109 | workflow: getRunsWorkflow, 110 | }); 111 | 112 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get runs'); 113 | expect(nodeResults.length).toBe(1); 114 | const [nodeResult] = nodeResults; 115 | expect(nodeResult.executionStatus).toBe('success'); 116 | 117 | const data = getTaskArrayData(nodeResult); 118 | expect(Array.isArray(data)).toBe(true); 119 | expect(data?.map((item) => item.json)).toEqual(mockRunsList.data.items); 120 | 121 | expect(scope.isDone()).toBe(true); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('actor-tasks', () => { 127 | describe('run-task', () => { 128 | it('should run the run-task workflow (waitForFinish: false)', async () => { 129 | const mockRunTask = fixtures.runActorResult(); 130 | 131 | const scope = nock('https://api.apify.com') 132 | .post('/v2/actor-tasks/PwUDLcG3zMyT8E4vq/runs') 133 | .query({ waitForFinish: 0, memory: 1024 }) 134 | .reply(200, mockRunTask); 135 | 136 | const runTaskWorkflow = require('./workflows/actor-tasks/run-task.workflow.json'); 137 | const { executionData } = await executeWorkflow({ 138 | credentialsHelper, 139 | workflow: runTaskWorkflow, 140 | }); 141 | 142 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run task'); 143 | expect(nodeResults.length).toBe(1); 144 | const [nodeResult] = nodeResults; 145 | expect(nodeResult.executionStatus).toBe('success'); 146 | 147 | const data = getTaskData(nodeResult); 148 | expect(data).toEqual(mockRunTask.data); 149 | 150 | expect(scope.isDone()).toBe(true); 151 | }); 152 | 153 | it('should run the run-task workflow and wait for finish (waitForFinish: true)', async () => { 154 | const mockRunTask = fixtures.runActorResult(); 155 | const mockFinishedRun = fixtures.getRunTaskResult(); 156 | 157 | const scope = nock('https://api.apify.com') 158 | .post('/v2/actor-tasks/PwUDLcG3zMyT8E4vq/runs') 159 | .query({ waitForFinish: 0, memory: 1024 }) 160 | .reply(200, mockRunTask) 161 | .get(`/v2/actor-runs/${mockRunTask.data.id}`) 162 | .reply(200, mockFinishedRun); 163 | 164 | const runTaskWorkflow = require('./workflows/actor-tasks/run-task-wait-for-finish.workflow.json'); 165 | const { executionData } = await executeWorkflow({ 166 | credentialsHelper, 167 | workflow: runTaskWorkflow, 168 | }); 169 | 170 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run task'); 171 | expect(nodeResults.length).toBe(1); 172 | const [nodeResult] = nodeResults; 173 | expect(nodeResult.executionStatus).toBe('success'); 174 | 175 | const data = getTaskData(nodeResult); 176 | // expect polled terminal run as result 177 | expect(data).not.toEqual(mockRunTask.data); 178 | expect(data).toEqual(mockFinishedRun.data); 179 | 180 | expect(scope.isDone()).toBe(true); 181 | }); 182 | }); 183 | 184 | describe('run-task-and-get-dataset', () => { 185 | it('should run the run-task-and-get-dataset workflow', async () => { 186 | const mockRunTask = fixtures.runActorResult(); 187 | const mockFinishedRun = fixtures.getSuccessRunResult(); 188 | const mockItems = fixtures.getItemsResult(); 189 | 190 | const datasetId = mockFinishedRun.data.defaultDatasetId; 191 | 192 | const scope = nock('https://api.apify.com') 193 | .post('/v2/actor-tasks/PwUDLcG3zMyT8E4vq/runs') 194 | .query({ waitForFinish: 0, memory: 1024 }) 195 | .reply(200, mockRunTask) 196 | .get(`/v2/actor-runs/${mockRunTask.data.id}`) 197 | .reply(200, mockFinishedRun) 198 | .get(`/v2/datasets/${datasetId}/items`) 199 | .query({ format: 'json' }) 200 | .reply(200, mockItems); 201 | 202 | const { executionData } = await executeWorkflow({ 203 | credentialsHelper, 204 | workflow: runTaskAndGetDatasetWorkflow, 205 | }); 206 | 207 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run task and get dataset'); 208 | expect(nodeResults.length).toBe(1); 209 | const [nodeResult] = nodeResults; 210 | expect(nodeResult.executionStatus).toBe('success'); 211 | 212 | const data = getTaskArrayData(nodeResult); 213 | expect(Array.isArray(data)).toBe(true); 214 | expect(data?.map((item) => item.json)).toEqual(mockItems); 215 | 216 | expect(scope.isDone()).toBe(true); 217 | }); 218 | 219 | it('should throw if the run-task-and-get-dataset workflow ends with ABORTED status', async () => { 220 | const mockRunTask = fixtures.runActorResult(); 221 | const mockAbortedRun = fixtures.getLastRunResult({ status: 'ABORTED' }); 222 | 223 | const scope = nock('https://api.apify.com') 224 | .post('/v2/actor-tasks/PwUDLcG3zMyT8E4vq/runs') 225 | .query({ waitForFinish: 0, memory: 1024 }) 226 | .reply(200, mockRunTask) 227 | .get(`/v2/actor-runs/${mockRunTask.data.id}`) 228 | .reply(200, mockAbortedRun); 229 | 230 | const { executionData } = await executeWorkflow({ 231 | credentialsHelper, 232 | workflow: runTaskAndGetDatasetWorkflow, 233 | }); 234 | 235 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run task and get dataset'); 236 | expect(nodeResults.length).toBe(1); 237 | const [nodeResult] = nodeResults; 238 | expect(nodeResult.executionStatus).toBe('error'); 239 | 240 | expect(scope.isDone()).toBe(true); 241 | }); 242 | }); 243 | }); 244 | 245 | describe('actors', () => { 246 | describe('get-last-run', () => { 247 | it('should run the get-last-run workflow', async () => { 248 | const mockLastRun = fixtures.getLastRunResult(); 249 | 250 | const scope = nock('https://api.apify.com') 251 | .get('/v2/acts/nFJndFXA5zjCTuudP/runs/last') 252 | .query({ status: 'ABORTED' }) 253 | .reply(200, mockLastRun); 254 | 255 | const getLastRunWorkflow = require('./workflows/actors/get-last-run.workflow.json'); 256 | const { executionData } = await executeWorkflow({ 257 | credentialsHelper, 258 | workflow: getLastRunWorkflow, 259 | }); 260 | 261 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get last run'); 262 | expect(nodeResults.length).toBe(1); 263 | const [nodeResult] = nodeResults; 264 | expect(nodeResult.executionStatus).toBe('success'); 265 | 266 | const data = getTaskData(nodeResult); 267 | expect(data).toEqual(mockLastRun.data); 268 | 269 | expect(scope.isDone()).toBe(true); 270 | }); 271 | }); 272 | describe('run-actor', () => { 273 | it('should run the run-actor workflow', async () => { 274 | const mockRunActor = fixtures.runActorResult(); 275 | const mockBuild = fixtures.getBuildResult(); 276 | 277 | const scope = nock('https://api.apify.com') 278 | .get('/v2/acts/nFJndFXA5zjCTuudP') 279 | .reply(200, fixtures.getActorResult()) 280 | .get('/v2/acts/nFJndFXA5zjCTuudP/builds/default') 281 | .reply(200, mockBuild) 282 | .post('/v2/acts/nFJndFXA5zjCTuudP/runs') 283 | .query({ waitForFinish: 0, build: mockBuild.data.buildNumber, memory: 1024 }) 284 | .reply(200, mockRunActor); 285 | 286 | const runActorWorkflow = require('./workflows/actors/run-actor.workflow.json'); 287 | const { executionData } = await executeWorkflow({ 288 | credentialsHelper, 289 | workflow: runActorWorkflow, 290 | }); 291 | 292 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run actor'); 293 | expect(nodeResults.length).toBe(1); 294 | const [nodeResult] = nodeResults; 295 | expect(nodeResult.executionStatus).toBe('success'); 296 | 297 | const data = getTaskData(nodeResult); 298 | expect(data).toEqual(mockRunActor.data); 299 | 300 | expect(scope.isDone()).toBe(true); 301 | }); 302 | 303 | it('should run the run-actor workflow and wait for finish', async () => { 304 | const mockRunActor = fixtures.runActorResult(); 305 | const mockBuild = fixtures.getBuildResult(); 306 | const mockFinishedRun = fixtures.getSuccessRunResult(); 307 | 308 | const scope = nock('https://api.apify.com') 309 | .get('/v2/acts/nFJndFXA5zjCTuudP') 310 | .reply(200, fixtures.getActorResult()) 311 | .get('/v2/acts/nFJndFXA5zjCTuudP/builds/default') 312 | .reply(200, mockBuild) 313 | .post('/v2/acts/nFJndFXA5zjCTuudP/runs') 314 | .query({ waitForFinish: 0, build: mockBuild.data.buildNumber, memory: 1024 }) 315 | .reply(200, mockRunActor) 316 | .get('/v2/actor-runs/Icz6E0IHX0c40yEi7') 317 | .reply(200, mockFinishedRun); 318 | 319 | const runActorWorkflow = require('./workflows/actors/run-actor-wait-for-finish.workflow.json'); 320 | const { executionData } = await executeWorkflow({ 321 | credentialsHelper, 322 | workflow: runActorWorkflow, 323 | }); 324 | 325 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run actor'); 326 | expect(nodeResults.length).toBe(1); 327 | const [nodeResult] = nodeResults; 328 | expect(nodeResult.executionStatus).toBe('success'); 329 | 330 | const data = getTaskData(nodeResult); 331 | // exptect polled terminal run as result 332 | expect(data).not.toEqual(mockRunActor.data); 333 | expect(data).toEqual(mockFinishedRun.data); 334 | 335 | expect(scope.isDone()).toBe(true); 336 | }); 337 | }); 338 | describe('run-actor-and-get-dataset', () => { 339 | it('should run the run-actor-and-get-dataset workflow', async () => { 340 | const mockRunActor = fixtures.runActorResult(); 341 | const mockBuild = fixtures.getBuildResult(); 342 | const mockFinishedRun = fixtures.getSuccessRunResult(); 343 | const mockItems = fixtures.getItemsResult(); 344 | 345 | const datasetId = mockFinishedRun.data.defaultDatasetId; 346 | 347 | const scope = nock('https://api.apify.com') 348 | .get('/v2/acts/nFJndFXA5zjCTuudP') 349 | .reply(200, fixtures.getActorResult()) 350 | .get('/v2/acts/nFJndFXA5zjCTuudP/builds/default') 351 | .reply(200, mockBuild) 352 | .post('/v2/acts/nFJndFXA5zjCTuudP/runs') 353 | .query({ waitForFinish: 0, build: mockBuild.data.buildNumber, memory: 1024 }) 354 | .reply(200, mockRunActor) 355 | .get(`/v2/actor-runs/${mockRunActor.data.id}`) 356 | .reply(200, mockFinishedRun) 357 | .get(`/v2/datasets/${datasetId}/items`) 358 | .query({ format: 'json' }) 359 | .reply(200, mockItems); 360 | 361 | const { executionData } = await executeWorkflow({ 362 | credentialsHelper, 363 | workflow: runActorAndGetDatasetWorkflow, 364 | }); 365 | 366 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run actor and get dataset'); 367 | expect(nodeResults.length).toBe(1); 368 | const [nodeResult] = nodeResults; 369 | expect(nodeResult.executionStatus).toBe('success'); 370 | 371 | const data = getTaskArrayData(nodeResult); 372 | expect(Array.isArray(data)).toBe(true); 373 | expect(data?.map((item) => item.json)).toEqual(mockItems); 374 | 375 | expect(scope.isDone()).toBe(true); 376 | }); 377 | 378 | it('should throw if the run-actor-and-get-dataset workflow ends with ABORTED status', async () => { 379 | const mockRunActor = fixtures.runActorResult(); 380 | const mockBuild = fixtures.getBuildResult(); 381 | const mockAbortedRun = fixtures.getLastRunResult({ status: 'ABORTED' }); 382 | 383 | const scope = nock('https://api.apify.com') 384 | .get('/v2/acts/nFJndFXA5zjCTuudP') 385 | .reply(200, fixtures.getActorResult()) 386 | .get('/v2/acts/nFJndFXA5zjCTuudP/builds/default') 387 | .reply(200, mockBuild) 388 | .post('/v2/acts/nFJndFXA5zjCTuudP/runs') 389 | .query({ waitForFinish: 0, build: mockBuild.data.buildNumber, memory: 1024 }) 390 | .reply(200, mockRunActor) 391 | .get(`/v2/actor-runs/${mockRunActor.data.id}`) 392 | .reply(200, mockAbortedRun); 393 | 394 | const { executionData } = await executeWorkflow({ 395 | credentialsHelper, 396 | workflow: runActorAndGetDatasetWorkflow, 397 | }); 398 | 399 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Run actor and get dataset'); 400 | expect(nodeResults.length).toBe(1); 401 | const [nodeResult] = nodeResults; 402 | expect(nodeResult.executionStatus).toBe('error'); 403 | 404 | expect(scope.isDone()).toBe(true); 405 | }); 406 | }); 407 | 408 | describe('scrape-single-url', () => { 409 | it('should run the scrape-single-url workflow', async () => { 410 | const mockRunActor = fixtures.runActorResult(); 411 | const mockFinishedRun = fixtures.getSuccessRunResult(); 412 | const mockItems = fixtures.getScrapeSingleUrlItemsResult(); 413 | 414 | const datasetId = mockFinishedRun.data.defaultDatasetId; 415 | 416 | const scope = nock('https://api.apify.com') 417 | .post(`/v2/acts/${helpers.consts.WEB_CONTENT_SCRAPER_ACTOR_ID}/runs`) 418 | .query({ waitForFinish: 0 }) 419 | .reply(200, mockRunActor) 420 | .get(`/v2/actor-runs/${mockRunActor.data.id}`) 421 | .reply(200, mockFinishedRun) 422 | .get(`/v2/datasets/${datasetId}/items`) 423 | .query({ format: 'json' }) 424 | .reply(200, mockItems); 425 | 426 | const scrapeSingleUrlWorkflow = require('./workflows/actors/scrape-single-url.workflow.json'); 427 | const { executionData } = await executeWorkflow({ 428 | credentialsHelper, 429 | workflow: scrapeSingleUrlWorkflow, 430 | }); 431 | 432 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Scrape single URL'); 433 | expect(nodeResults.length).toBe(1); 434 | const [nodeResult] = nodeResults; 435 | expect(nodeResult.executionStatus).toBe('success'); 436 | 437 | const data = getTaskData(nodeResult); 438 | expect(typeof data).toBe('object'); 439 | const { text, ...mockedItemWithoutText } = mockItems[0]; 440 | expect(data).toEqual(mockedItemWithoutText); 441 | 442 | expect(scope.isDone()).toBe(true); 443 | }); 444 | }); 445 | }); 446 | 447 | describe('datasets', () => { 448 | describe('get-items', () => { 449 | it('should run the get-items workflow', async () => { 450 | const mockItems = fixtures.getItemsResult(); 451 | const datasetId = 'WkzbQMuFYuamGv3YF'; 452 | 453 | const scope = nock('https://api.apify.com') 454 | .get(`/v2/datasets/${datasetId}/items`) 455 | .query(true) 456 | .reply(200, mockItems); 457 | 458 | const getItemsWorkflow = require('./workflows/datasets/get-items.workflow.json'); 459 | const { executionData } = await executeWorkflow({ 460 | credentialsHelper, 461 | workflow: getItemsWorkflow, 462 | }); 463 | 464 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get items'); 465 | expect(nodeResults.length).toBe(1); 466 | const [nodeResult] = nodeResults; 467 | expect(nodeResult.executionStatus).toBe('success'); 468 | 469 | const data = getTaskArrayData(nodeResult); 470 | expect(Array.isArray(data)).toBe(true); 471 | expect(data?.map((item) => item.json)).toEqual(mockItems); 472 | 473 | expect(scope.isDone()).toBe(true); 474 | }); 475 | }); 476 | }); 477 | 478 | describe('key-value-stores', () => { 479 | describe('get-key-value-store-record', () => { 480 | it('should run the get-key-value-store-record workflow', async () => { 481 | const mockRecord = fixtures.getKeyValueStoreRecordResult(); 482 | const storeId = 'yTfMu13hDFe9bRjx6'; 483 | const recordKey = 'INPUT'; 484 | 485 | const scope = nock('https://api.apify.com') 486 | .get(`/v2/key-value-stores/${storeId}/records/${recordKey}`) 487 | .reply(200, mockRecord); 488 | 489 | const getKeyValueStoreRecordWorkflow = require('./workflows/key-value-stores/get-key-value-store-record.workflow.json'); 490 | const { executionData } = await executeWorkflow({ 491 | credentialsHelper, 492 | workflow: getKeyValueStoreRecordWorkflow, 493 | }); 494 | 495 | const nodeResults = getRunTaskDataByNodeName(executionData, 'Get Key-Value Store Record'); 496 | expect(nodeResults.length).toBe(1); 497 | const [nodeResult] = nodeResults; 498 | expect(nodeResult.executionStatus).toBe('success'); 499 | 500 | const data = getTaskData(nodeResult); 501 | expect(data).toEqual({ 502 | storeId, 503 | recordKey, 504 | contentType: expect.any(String), 505 | data: mockRecord, 506 | }); 507 | 508 | expect(scope.isDone()).toBe(true); 509 | }); 510 | }); 511 | }); 512 | 513 | describe('api calls', () => { 514 | it('should retry the specified number of times with exponential delays', async () => { 515 | const storeId = 'yTfMu13hDFe9bRjx6'; 516 | const recordKey = 'INPUT'; 517 | 518 | const scope = nock('https://api.apify.com') 519 | .get(`/v2/key-value-stores/${storeId}/records/${recordKey}`) 520 | .reply(500) 521 | .get(`/v2/key-value-stores/${storeId}/records/${recordKey}`) 522 | .reply(429) 523 | .get(`/v2/key-value-stores/${storeId}/records/${recordKey}`) 524 | .reply(200); 525 | 526 | const getKeyValueStoreRecordWorkflow = require('./workflows/key-value-stores/get-key-value-store-record.workflow.json'); 527 | await executeWorkflow({ 528 | credentialsHelper, 529 | workflow: getKeyValueStoreRecordWorkflow, 530 | }); 531 | 532 | expect(scope.isDone()).toBe(true); 533 | }); 534 | }); 535 | }); 536 | --------------------------------------------------------------------------------