Skip to content

Commit eb5c553

Browse files
authored
feat: poll run status in run-actor (#48)
* feat: poll run status in run-actor * test: add test for run-actor with wait-for-finish * feat: replace node timeout with n8n timeout * refactor: change run-actor related comments
1 parent 24e806b commit eb5c553

File tree

7 files changed

+235
-37
lines changed

7 files changed

+235
-37
lines changed

nodes/Apify/__tests__/Apify.node.spec.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ describe('Apify Node', () => {
157157

158158
const scope = nock('https://api.apify.com')
159159
.get('/v2/acts/nFJndFXA5zjCTuudP')
160-
.reply(200, { data: fixtures.getActorResult() })
160+
.reply(200, fixtures.getActorResult())
161161
.get('/v2/acts/nFJndFXA5zjCTuudP/builds/default')
162-
.reply(200, { data: fixtures.getBuildResult() })
162+
.reply(200, fixtures.getBuildResult())
163163
.post('/v2/acts/nFJndFXA5zjCTuudP/runs')
164-
.query({ waitForFinish: 60 })
164+
.query({ waitForFinish: 0 })
165165
.reply(200, mockRunActor);
166166

167167
const runActorWorkflow = require('./workflows/actors/run-actor.workflow.json');
@@ -181,6 +181,41 @@ describe('Apify Node', () => {
181181

182182
expect(scope.isDone()).toBe(true);
183183
});
184+
185+
it('should run the run-actor workflow and wait for finish', async () => {
186+
const mockRunActor = fixtures.runActorResult();
187+
const mockFinishedRun = fixtures.getRunResult();
188+
189+
const scope = nock('https://api.apify.com')
190+
.get('/v2/acts/nFJndFXA5zjCTuudP')
191+
.reply(200, fixtures.getActorResult())
192+
.get('/v2/acts/nFJndFXA5zjCTuudP/builds/default')
193+
.reply(200, fixtures.getBuildResult())
194+
.post('/v2/acts/nFJndFXA5zjCTuudP/runs')
195+
.query({ waitForFinish: 0 })
196+
.reply(200, mockRunActor)
197+
.get('/v2/actor-runs/Icz6E0IHX0c40yEi7')
198+
.reply(200, mockFinishedRun);
199+
200+
const runActorWorkflow = require('./workflows/actors/run-actor-wait-for-finish.workflow.json');
201+
const { waitPromise } = await executeWorkflow({
202+
credentialsHelper,
203+
workflow: runActorWorkflow,
204+
});
205+
const result = await waitPromise.promise();
206+
207+
const nodeResults = getRunTaskDataByNodeName(result, 'Run actor');
208+
expect(nodeResults.length).toBe(1);
209+
const [nodeResult] = nodeResults;
210+
expect(nodeResult.executionStatus).toBe('success');
211+
212+
const data = getTaskData(nodeResult);
213+
// exptect polled terminal run as result
214+
expect(data).not.toEqual(mockRunActor.data);
215+
expect(data).toEqual(mockFinishedRun.data);
216+
217+
expect(scope.isDone()).toBe(true);
218+
});
184219
});
185220
describe('scrape-single-url', () => {
186221
it('should run the scrape-single-url workflow', async () => {

nodes/Apify/__tests__/utils/fixtures.ts

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -243,52 +243,135 @@ export const getLastRunResult = () => {
243243
};
244244
};
245245

246+
// without waitForFinish (waitForFinish = 0)
246247
export const runActorResult = () => {
247248
return {
248249
data: {
249-
id: 'gbNI2e1oNCufgd5Rn',
250+
id: 'Icz6E0IHX0c40yEi7',
250251
actId: 'nFJndFXA5zjCTuudP',
251252
userId: 'A9zwKYff2yyRmaqc9',
252-
startedAt: '2025-06-16T23:30:42.255Z',
253-
finishedAt: '2025-06-16T23:30:52.922Z',
253+
startedAt: '2025-06-30T12:36:08.502Z',
254+
finishedAt: null,
255+
status: 'READY',
256+
meta: {
257+
origin: 'API',
258+
userAgent: 'axios/1.7.4',
259+
},
260+
stats: {
261+
inputBodyLen: 346,
262+
migrationCount: 0,
263+
rebootCount: 0,
264+
restartCount: 0,
265+
resurrectCount: 0,
266+
computeUnits: 0,
267+
},
268+
options: {
269+
build: 'latest',
270+
timeoutSecs: 604800,
271+
memoryMbytes: 1024,
272+
maxTotalChargeUsd: 37.42362467983089,
273+
diskMbytes: 2048,
274+
},
275+
buildId: 'DgGC7ZxWmZ0cnuNIy',
276+
defaultKeyValueStoreId: 'dgt7oov3cthGsD4yq',
277+
defaultDatasetId: '63kMAihbWVgBvEAZ2',
278+
defaultRequestQueueId: 'cGohS4eFRrm2mItLx',
279+
pricingInfo: {
280+
pricingModel: 'PAY_PER_EVENT',
281+
reasonForChange:
282+
'We are introducing Store pricing discounts for this Actor and a new pricing model to give you more transparency and flexibility; more info in the follow-up email.',
283+
minimalMaxTotalChargeUsd: 0.5,
284+
createdAt: '2025-05-29T14:45:00.000Z',
285+
startedAt: '2025-06-10T08:00:00.000Z',
286+
apifyMarginPercentage: 0,
287+
notifiedAboutChangeAt: '2025-06-10T08:00:00.000Z',
288+
pricingPerEvent: {
289+
actorChargeEvents: {
290+
'actor-start': {
291+
eventTitle: 'Actor start',
292+
eventDescription: 'Flat fee for starting an Actor run.',
293+
eventPriceUsd: 0.0015,
294+
},
295+
'search-page-scraped': {
296+
eventTitle: 'Search results page scraped',
297+
eventDescription: 'Cost per page of Google Search results successfully scraped.',
298+
eventPriceUsd: 0.0035,
299+
},
300+
'ads-scraped': {
301+
eventTitle: 'Add-on: Paid results (ads) extraction',
302+
eventDescription:
303+
'Extra cost per page for attempting to extract paid results (ads) from Google Search. This applies when the ads extraction feature is enabled, regardless of whether ads are found on the specific page.',
304+
eventPriceUsd: 0.005,
305+
},
306+
},
307+
},
308+
},
309+
chargedEventCounts: {
310+
'actor-start': 0,
311+
'search-page-scraped': 0,
312+
'ads-scraped': 0,
313+
},
314+
platformUsageBillingModel: 'DEVELOPER',
315+
accountedChargedEventCounts: {
316+
'actor-start': 0,
317+
'search-page-scraped': 0,
318+
'ads-scraped': 0,
319+
},
320+
generalAccess: 'FOLLOW_USER_SETTING',
321+
buildNumber: '0.0.166',
322+
containerUrl: 'https://fttjdkagbv7c.runs.apify.net',
323+
usageTotalUsd: 0,
324+
},
325+
};
326+
};
327+
328+
export const getSuccessRunResult = () => {
329+
return {
330+
data: {
331+
id: 'ZtmMxsnaxohefirDg',
332+
actId: 'nFJndFXA5zjCTuudP',
333+
userId: 'A9zwKYff2yyRmaqc9',
334+
startedAt: '2025-06-30T12:35:16.689Z',
335+
finishedAt: '2025-06-30T12:35:24.942Z',
254336
status: 'SUCCEEDED',
255-
statusMessage: 'Finished! Total 2 requests: 2 succeeded, 0 failed.',
337+
statusMessage:
338+
'Actor finished successfully. Processed 3 queries on 3 pages. Extracted: 110 organicResults, 0 paidResults, 0 paidProducts, 48 relatedQueries, 1 aiOverviews.',
256339
isStatusMessageTerminal: true,
257340
meta: {
258341
origin: 'API',
259342
userAgent: 'axios/1.7.4',
260343
},
261344
stats: {
262-
inputBodyLen: 336,
345+
inputBodyLen: 346,
263346
migrationCount: 0,
264347
rebootCount: 0,
265348
restartCount: 0,
266-
durationMillis: 10476,
349+
durationMillis: 8137,
267350
resurrectCount: 0,
268-
runTimeSecs: 10.476,
351+
runTimeSecs: 8.137,
269352
metamorph: 0,
270-
computeUnits: 0.00291,
271-
memAvgBytes: 128424657.84661157,
272-
memMaxBytes: 145063936,
273-
memCurrentBytes: 0,
274-
cpuAvgUsage: 15.432343907121574,
275-
cpuMaxUsage: 91.47420111042567,
276-
cpuCurrentUsage: 0,
277-
netRxBytes: 620882,
278-
netTxBytes: 467443,
353+
computeUnits: 0.0022602777777777777,
354+
memAvgBytes: 114593891.31194456,
355+
memMaxBytes: 171585536,
356+
memCurrentBytes: 171585536,
357+
cpuAvgUsage: 41.576489958372534,
358+
cpuMaxUsage: 112.53407249466952,
359+
cpuCurrentUsage: 59.16827757125155,
360+
netRxBytes: 539776,
361+
netTxBytes: 503865,
279362
},
280363
options: {
281364
build: 'latest',
282365
timeoutSecs: 604800,
283366
memoryMbytes: 1024,
284-
maxTotalChargeUsd: 36.57444567272754,
367+
maxTotalChargeUsd: 37.44156455943874,
285368
diskMbytes: 2048,
286369
},
287-
buildId: 'da2D8ovPHWBN98zj2',
370+
buildId: 'DgGC7ZxWmZ0cnuNIy',
288371
exitCode: 0,
289-
defaultKeyValueStoreId: 'K1AdT7nsdFw2ThD5J',
290-
defaultDatasetId: '6kTQQ34S3DftNqbE5',
291-
defaultRequestQueueId: '2cPPDy1XSk5k17yst',
372+
defaultKeyValueStoreId: '4aoPkdUdfoRpf9w7E',
373+
defaultDatasetId: 'U0hEU6N57UfDgqI98',
374+
defaultRequestQueueId: 'qNGmAgPf3Pb50PhqC',
292375
pricingInfo: {
293376
pricingModel: 'PAY_PER_EVENT',
294377
reasonForChange:
@@ -331,9 +414,10 @@ export const runActorResult = () => {
331414
'ads-scraped': 0,
332415
},
333416
generalAccess: 'FOLLOW_USER_SETTING',
334-
buildNumber: '0.0.165',
335-
containerUrl: 'https://xds5z9sxx8sx.runs.apify.net',
417+
buildNumber: '0.0.166',
418+
containerUrl: 'https://mc9gwqvhjshq.runs.apify.net',
336419
usageTotalUsd: 0.0015,
420+
consoleUrl: 'https://console.apify.com/view/runs/ZtmMxsnaxohefirDg',
337421
},
338422
};
339423
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
}

nodes/Apify/__tests__/workflows/actors/run-actor.workflow.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"cachedResultName": "Google Search Results Scraper",
1212
"cachedResultUrl": "https://console.com/actors/nFJndFXA5zjCTuudP/input"
1313
},
14-
"waitForFinish": 60,
14+
"waitForFinish": false,
1515
"customBody": "{\n \"maxPagesPerQuery\": 1,\n \"resultsPerPage\": 2,\n \"queries\": \"javascript\\ntypescript\"\n}"
1616
},
1717
"id": "8b21ab5d-baff-48f1-9512-831704fd2df4",

nodes/Apify/helpers/consts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export const WEB_CONTENT_SCRAPER_ACTOR_ID = 'aYG0l9s7dbB7j3gbS';
22
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; //

nodes/Apify/resources/actors/run-actor/execute.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
NodeOperationError,
66
} from 'n8n-workflow';
77
import { apiRequest } from '../../genericFunctions';
8+
import * as helpers from '../../../helpers';
89

910
export async function runActor(this: IExecuteFunctions, i: number): Promise<INodeExecutionData> {
1011
const actorId = this.getNodeParameter('actorId', i, undefined, {
@@ -13,7 +14,7 @@ export async function runActor(this: IExecuteFunctions, i: number): Promise<INod
1314
const timeout = this.getNodeParameter('timeout', i) as number | null;
1415
const memory = this.getNodeParameter('memory', i) as number | null;
1516
const buildParam = this.getNodeParameter('build', i) as string | null;
16-
const waitForFinish = this.getNodeParameter('waitForFinish', i) as number | null;
17+
const waitForFinish = this.getNodeParameter('waitForFinish', i) as boolean;
1718
const rawStringifiedInput = this.getNodeParameter('customBody', i, '{}') as string;
1819

1920
let userInput: any;
@@ -61,13 +62,50 @@ export async function runActor(this: IExecuteFunctions, i: number): Promise<INod
6162
if (timeout != null) qs.timeout = timeout;
6263
if (memory != null) qs.memory = memory;
6364
if (build?.buildTag) qs.build = build.buildTag;
64-
if (waitForFinish != null) qs.waitForFinish = waitForFinish;
65+
qs.waitForFinish = 0; // set initial run actor to not wait for finish
6566

6667
// 6. Run the actor
6768
const run = await runActorApi.call(this, actorId, mergedInput, qs);
69+
if (!run?.data?.id) {
70+
throw new NodeApiError(this.getNode(), {
71+
message: `Run ID not found after running the actor`,
72+
});
73+
}
74+
75+
// 7a. If waitForFinish is false, return the run data immediately
76+
if (!waitForFinish) {
77+
return {
78+
json: { ...run.data },
79+
};
80+
}
6881

82+
// 7b. Start polling for run status until it reaches a terminal state
83+
// This loop is infinite and will only stop when a terminal status is reached,
84+
// or when the workflow maximum timeout is hit, as set in your n8n configuration.
85+
const runId = run.data.id;
86+
let lastRunData = run.data;
87+
while (true) {
88+
try {
89+
const pollResult = await apiRequest.call(this, {
90+
method: 'GET',
91+
uri: `/v2/actor-runs/${runId}`,
92+
});
93+
const status = pollResult?.data?.status;
94+
lastRunData = pollResult?.data;
95+
if (helpers.consts.TERMINAL_RUN_STATUSES.includes(status)) {
96+
break;
97+
}
98+
} catch (err) {
99+
throw new NodeApiError(this.getNode(), {
100+
message: `Error polling run status: ${err}`,
101+
});
102+
}
103+
await new Promise((resolve) =>
104+
setTimeout(resolve, helpers.consts.WAIT_FOR_FINISH_POLL_INTERVAL),
105+
);
106+
}
69107
return {
70-
json: { ...run.data },
108+
json: { ...lastRunData },
71109
};
72110
}
73111

nodes/Apify/resources/actors/run-actor/properties.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,9 @@ configuration for the Actor (typically \`latest\`).`,
103103
displayName: 'Wait for Finish',
104104
name: 'waitForFinish',
105105
description:
106-
"The maximum number of seconds the server waits for the run to finish. By default the server doesn't wait for the run to finish and returns immediately. The maximum value is 60 seconds.",
107-
default: null,
108-
type: 'number',
109-
typeOptions: {
110-
minValue: 0,
111-
maxValue: 60,
112-
},
106+
'Whether 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.',
107+
default: true,
108+
type: 'boolean',
113109
displayOptions: {
114110
show: {
115111
resource: ['Actors'],

0 commit comments

Comments
 (0)