You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
call-actor in default mode blocks indefinitely until the Actor finishes. This is problematic for long-running Actors — the tool call can hang for minutes or hours. Additionally, the response shape differs between sync and async modes, the dataset preview is misleading for arbitrary Actors, and the tool surface (get-actor-run / get-actor-output / get-dataset-items) has confusing overlap.
This issue replaces #579 with a cleaner approach derived from detailed analysis. The core idea:
call-actoralways starts the Actor and returns immediately with run metadata (uniform shape)
In MCP task mode, call-actor waits internally for completion (task stays "working" with progress updates)
get-actor-run gains a waitSecs parameter for non-task clients to do bounded server-side waits
get-actor-output becomes the single way to get actual data
Each tool has one clear role: start → wait → get data
Original PR #594 closed in favor of the split. Out-of-scope follow-ups tracked in #587.
Scope
In scope
Refactor call-actor (default mode) to always return immediately with uniform run metadata
Add internal task-mode wait: when running inside an MCP task, call-actor waits for the Actor to reach a terminal state before returning (task stays "working" with progress updates)
Add waitSecs parameter to get-actor-run (default 10s, max 60s)
Remove async and previewOutput parameters from call-actor schema
Remove dataset preview from both call-actor and get-actor-run responses
Add storages (defaultDatasetId, defaultKeyValueStoreId) and hint to both tools' structuredContent
Add storages and hint to OpenAI mode call-actor structuredContent
New actorRunOutputSchema shared by both call-actor and get-actor-run
Split schemas: new schema for call-actor, old schema stays for direct actor tools
Update tool descriptions, server instructions, and output schemas
Widget compatibility: Update ActorRun widget to pass waitSecs: 0 in polling calls
Cancellation: Ensure task-mode call-actor cancellation properly aborts the Actor run via abort signal chain
Docs: Update README to reflect new workflow
Unit + integration test updates
Out of scope
Direct actor tool changes (RAG web browser, etc. keep blocking + old response shape) — follow-up
get-actor-output accepting runId as alternative to datasetId — follow-up
taskSupport on get-actor-run — deferred
Resource links in tool results (future)
Dynamic server resources + resource/list_changed (future)
Auto-wait for some clients (future)
Technical design
Overview
Two changes to existing tools, no new tools needed:
call-actor removes the sync wait path entirely. Always calls actorClient.start() and returns immediately. In MCP task mode (detected via server-internal mcpTaskExecution flag on InternalToolArgs), it additionally calls waitForFinish() before returning — the MCP task system wraps this in a background promise, so the client sees a "working" task with progress updates.
get-actor-run adds optional waitSecs parameter (default 10, max 60). When > 0, uses apifyClient.run(runId).waitForFinish({ waitSecs }) which polls the run at short intervals and returns immediately when the run reaches a terminal state — waitSecs is a maximum wait time, not a fixed delay. A 3-second Actor with waitSecs: 10 returns in ~3 seconds, not 10.
call-actor → {status: RUNNING, hint: "Use get-actor-run to wait for the Actor run to finish"}
→ get-actor-run(runId, waitSecs:10) → {status: RUNNING, hint: "Actor is still running, call get-actor-run again"}
→ get-actor-run(runId, waitSecs:10) → {status: SUCCEEDED, hint: "Use get-actor-output to retrieve Actor run results"}
→ get-actor-output(datasetId) → data
The ActorRun widget reads them for display (computeUnits, memoryAvgBytes, memoryMaxBytes)
They're lightweight diagnostic fields already present in the run object
call-actor naturally doesn't have them (run just started)
Hint values by status (always reference Apify tool names):
RUNNING / READY → "Use get-actor-run to wait for the Actor run to finish"
SUCCEEDED → "Use get-actor-output with the datasetId from storages to retrieve Actor run results"
FAILED / ABORTED / TIMED-OUT → "Actor run failed. Use get-actor-log to inspect what went wrong"
1. mcpTaskExecution flag (server-internal)
Add mcpTaskExecution?: boolean to InternalToolArgs in src/types.ts. This is a server-internal flag — NOT a tool input parameter. The server sets it in executeToolAndUpdateTask when the client requested task-augmented execution (tools/call with task params). The LLM never sees it.
2. call-actor always returns immediately
src/tools/default/call_actor.ts: Remove the entire sync path (callActorGetDataset call). Always call actorClient.start(). In task mode (mcpTaskExecution === true), additionally call waitForFinish() with abort signal racing — the MCP task wraps this in a background promise.
src/tools/core/call_actor_common.ts:
Remove async and previewOutput parameters from Zod schema entirely
Extract waitForRunWithAbort() helper (reuse abort-racing pattern from actor_execution.ts)
3. get-actor-run with waitSecs
src/tools/core/get_actor_run_common.ts:
Add waitSecs to schema: z.number().int().min(0).max(60).default(10) — "Maximum seconds to wait for the Actor run to finish (default 10, max 60). The server polls the run at short intervals and returns immediately when a terminal state is reached — waitSecs is a ceiling, not a fixed delay. If the run is still active after this time, returns current status — call again to continue waiting. Use 0 for an instant status check without waiting."
Remove dataset preview fetching (the 5-item preview + schema generation)
Keep finishedAt and stats as optional fields (widget needs them)
Add storages and hint to response
4. Schema split
src/tools/structured_output_schemas.ts:
Add new actorRunOutputSchema for call-actor (base shape)
Add new getActorRunOutputSchema for get-actor-run (base + finishedAt/stats, minus old dataset)
Keep callActorOutputSchema and buildEnrichedCallActorOutputSchema unchanged — direct actor tools still use them
5. Widget compatibility
src/web/src/pages/ActorRun/ActorRun.tsx:
Update both get-actor-run calls (lines ~331 and ~394) to pass waitSecs: 0 for instant status checks
Remove dependency on dataset field from structuredContent (no more preview items from get-actor-run)
stats and finishedAt continue to work (kept in response)
6. Cancellation in task mode
src/tools/core/call_actor_common.ts — the waitForRunWithAbort() helper:
Races waitForFinish() against the abort signal
On abort: calls apifyClient.run(runId).abort() to stop the Actor on Apify
Returns null to indicate cancellation (caller returns empty response)
The abort signal chain: client cancels task → server's abortController.abort() → tool's abort signal fires → waitForRunWithAbort aborts the Apify run → task transitions to cancelled.
7. README update
Update README.md (~lines 208, 230) to reflect the new workflow:
Remove references to sync results / preview from call-actor
Document the start → wait → get-data pattern
Update tool disambiguation section
Internal repo impact
apify-mcp-server-internal does not import any of the changed functions (callActorGetDataset, buildActorResponseContent, callActorOutputSchema, getActorRunOutputSchema, fetchActorRunData). These are all internal implementation details not exported from index_internals.ts.
The internal server imports only: ActorsMcpServer, ApifyClient, HelperTools, getServerCard, tool registry functions, and utilities — none affected.
Internal integration tests exercise call-actor through the tool call layer and will see the new response shape. Assertion helpers in the internal test suite should be verified after the public package is updated.
No changes needed in internal repo, but verify after merging.
Breaking changes
Change
Impact
call-actor no longer blocks and returns dataset preview
Clients expecting dataset items in response will get run metadata instead
async parameter removed
Clients passing async: true/false get validation error
previewOutput parameter removed
Clients passing previewOutput get validation error
callActorOutputSchema shape unchanged
Direct actor tools unaffected
get-actor-run drops dataset preview
Clients expecting dataset field in response won't find it
get-actor-run adds waitSecs with default 10
Existing calls without waitSecs now wait up to 10s instead of returning instantly. Widget updated to pass waitSecs: 0.
These are acceptable per project constraints: breaking changes are OK when they produce a simpler, clearer implementation.
Testing strategy
Unit tests
Update tests/unit/tools.structured_output_schemas.test.ts — add tests for actorRunOutputSchema, keep buildEnrichedCallActorOutputSchema tests
Update tests/unit/tools.mode_contract.test.ts — description text changes
Test getHintForStatus() for all run statuses
Test buildCallActorStructuredContent() output format
Test waitForRunWithAbort() — abort signal cancels wait and aborts run
Integration tests (in tests/integration/suite.ts)
Update sync/async call-actor tests → assert new uniform shape with storages/hint
Delete previewOutput tests (param removed)
Delete preview items tests (no preview anymore)
Update task mode test → assert status: SUCCEEDED, hint contains "get-actor-output"
Update get-actor-run tests → validate against new schema, storages/hint
NEW: Test waitSecs on get-actor-run — start actor, wait with waitSecs: 30, assert terminal status
NEW: Test waitSecs: 0 returns immediately without waiting
Verify cancellation aborts the Actor run
Manual testing
Claude Desktop (stdio): verify LLM learns new workflow (call-actor → get-actor-run → get-actor-output)
MCPJam inspector: verify structuredContent format
ChatGPT (openai mode): verify widget still works — polls with waitSecs: 0, displays stats/finishedAt
Verification checklist
npm run type-check passes
npm run lint passes
npm run test:unit passes
Widget polling works with waitSecs: 0
Widget displays stats and finishedAt correctly
Task-mode cancellation aborts the Actor run
README updated
Internal repo impact assessed ✅ (no changes needed)
Context and motivation
call-actorin default mode blocks indefinitely until the Actor finishes. This is problematic for long-running Actors — the tool call can hang for minutes or hours. Additionally, the response shape differs between sync and async modes, the dataset preview is misleading for arbitrary Actors, and the tool surface (get-actor-run/get-actor-output/get-dataset-items) has confusing overlap.This issue replaces #579 with a cleaner approach derived from detailed analysis. The core idea:
call-actoralways starts the Actor and returns immediately with run metadata (uniform shape)call-actorwaits internally for completion (task stays "working" with progress updates)get-actor-rungains awaitSecsparameter for non-task clients to do bounded server-side waitsget-actor-outputbecomes the single way to get actual dataSupersedes #579.
Scope
In scope
call-actor(default mode) to always return immediately with uniform run metadatacall-actorwaits for the Actor to reach a terminal state before returning (task stays "working" with progress updates)waitSecsparameter toget-actor-run(default 10s, max 60s)asyncandpreviewOutputparameters fromcall-actorschemacall-actorandget-actor-runresponsesstorages(defaultDatasetId, defaultKeyValueStoreId) andhintto both tools' structuredContentstoragesandhintto OpenAI modecall-actorstructuredContentactorRunOutputSchemashared by bothcall-actorandget-actor-runcall-actor, old schema stays for direct actor toolswaitSecs: 0in polling callscall-actorcancellation properly aborts the Actor run via abort signal chainOut of scope
get-actor-outputacceptingrunIdas alternative todatasetId— follow-uptaskSupportonget-actor-run— deferredresource/list_changed(future)Technical design
Overview
Two changes to existing tools, no new tools needed:
call-actorremoves the sync wait path entirely. Always callsactorClient.start()and returns immediately. In MCP task mode (detected via server-internalmcpTaskExecutionflag onInternalToolArgs), it additionally callswaitForFinish()before returning — the MCP task system wraps this in a background promise, so the client sees a "working" task with progress updates.get-actor-runadds optionalwaitSecsparameter (default 10, max 60). When > 0, usesapifyClient.run(runId).waitForFinish({ waitSecs })which polls the run at short intervals and returns immediately when the run reaches a terminal state —waitSecsis a maximum wait time, not a fixed delay. A 3-second Actor withwaitSecs: 10returns in ~3 seconds, not 10.Client flows
Task client (2 tool calls):
Non-task client (3+ tool calls):
OpenAI/widget (1 tool call + widget polling):
Sequence diagrams
Task client
sequenceDiagram participant LLM participant Client participant Server participant Apify LLM->>Client: call-actor(web-scraper, input) Client->>Server: tools/call + task params Server->>Apify: actor.start(input) Apify-->>Server: ActorRun (RUNNING) Server-->>Client: Task created (taskId) Note over Server: Background: waitForFinish() loop Task polling (MCP protocol) Client->>Server: tasks/get Server-->>Client: status: working, "Actor running (15s)..." end Apify-->>Server: Run finished (SUCCEEDED) Server-->>Client: task: completed Client->>Server: tasks/result Server-->>Client: {runId, status: SUCCEEDED, storages, hint} Client-->>LLM: Result with storages LLM->>Client: get-actor-output(datasetId) Client->>Server: tools/call Server->>Apify: dataset.listItems() Apify-->>Server: items Server-->>Client: {items, totalItemCount} Client-->>LLM: DataNon-task client
sequenceDiagram participant LLM participant Client participant Server participant Apify LLM->>Client: call-actor(web-scraper, input) Client->>Server: tools/call (no task) Server->>Apify: actor.start(input) Apify-->>Server: ActorRun (RUNNING) Server-->>Client: {runId, status: RUNNING, storages, hint} Client-->>LLM: "Use get-actor-run to wait" LLM->>Client: get-actor-run(runId, waitSecs: 10) Client->>Server: tools/call Server->>Apify: run.waitForFinish({waitSecs: 10}) Note over Server,Apify: Polls run status, returns early if done Apify-->>Server: Still RUNNING (timeout reached) Server-->>Client: {status: RUNNING, hint: "call get-actor-run again"} Client-->>LLM: Still running LLM->>Client: get-actor-run(runId, waitSecs: 10) Client->>Server: tools/call Server->>Apify: run.waitForFinish({waitSecs: 10}) Apify-->>Server: SUCCEEDED (returned early) Server-->>Client: {status: SUCCEEDED, storages, hint} Client-->>LLM: "Use get-actor-output" LLM->>Client: get-actor-output(datasetId) Client->>Server: tools/call Server-->>Client: {items, totalItemCount} Client-->>LLM: DataOpenAI/widget
sequenceDiagram participant LLM participant Client participant Server participant Apify participant Widget LLM->>Client: call-actor(web-scraper, input) Client->>Server: tools/call Server->>Apify: actor.start(input) Apify-->>Server: ActorRun (RUNNING) Server-->>Client: {runId, storages, hint} + widget meta Client-->>LLM: Result + widget renders loop Widget auto-polling (waitSecs: 0) Widget->>Server: get-actor-run(runId, waitSecs: 0) Server->>Apify: run.get() Apify-->>Server: status Server-->>Widget: {status, storages, stats?, finishedAt?} end Note over Widget: Shows completion in UI Note over LLM: Done — widget handled everythingCancellation in task mode
sequenceDiagram participant Client participant Server participant Apify Client->>Server: tools/call + task (call-actor) Server->>Apify: actor.start(input) Server-->>Client: Task created (taskId) Note over Server: Background: waitForFinish() Client->>Server: tasks/cancel(taskId) Server->>Server: abortController.abort() Note over Server: waitForRunWithAbort detects abort Server->>Apify: run.abort() Note over Server: Task transitions to cancelledDetailed design
Response shapes
call-actorresponse (base shape):get-actor-runresponse (base + diagnostic fields):get-actor-runkeepsfinishedAtandstatsbecause:call-actornaturally doesn't have them (run just started)Hint values by status (always reference Apify tool names):
RUNNING/READY→ "Useget-actor-runto wait for the Actor run to finish"SUCCEEDED→ "Useget-actor-outputwith thedatasetIdfrom storages to retrieve Actor run results"FAILED/ABORTED/TIMED-OUT→ "Actor run failed. Useget-actor-logto inspect what went wrong"1.
mcpTaskExecutionflag (server-internal)Add
mcpTaskExecution?: booleantoInternalToolArgsinsrc/types.ts. This is a server-internal flag — NOT a tool input parameter. The server sets it inexecuteToolAndUpdateTaskwhen the client requested task-augmented execution (tools/callwithtaskparams). The LLM never sees it.2.
call-actoralways returns immediatelysrc/tools/default/call_actor.ts: Remove the entire sync path (callActorGetDatasetcall). Always callactorClient.start(). In task mode (mcpTaskExecution === true), additionally callwaitForFinish()with abort signal racing — the MCP task wraps this in a background promise.src/tools/core/call_actor_common.ts:asyncandpreviewOutputparameters from Zod schema entirelybuildCallActorStructuredContent()shared response buildergetHintForStatus()hint mapperwaitForRunWithAbort()helper (reuse abort-racing pattern fromactor_execution.ts)3.
get-actor-runwithwaitSecssrc/tools/core/get_actor_run_common.ts:waitSecsto schema:z.number().int().min(0).max(60).default(10)— "Maximum seconds to wait for the Actor run to finish (default 10, max 60). The server polls the run at short intervals and returns immediately when a terminal state is reached — waitSecs is a ceiling, not a fixed delay. If the run is still active after this time, returns current status — call again to continue waiting. Use 0 for an instant status check without waiting."finishedAtandstatsas optional fields (widget needs them)storagesandhintto response4. Schema split
src/tools/structured_output_schemas.ts:actorRunOutputSchemaforcall-actor(base shape)getActorRunOutputSchemaforget-actor-run(base + finishedAt/stats, minus old dataset)callActorOutputSchemaandbuildEnrichedCallActorOutputSchemaunchanged — direct actor tools still use them5. Widget compatibility
src/web/src/pages/ActorRun/ActorRun.tsx:get-actor-runcalls (lines ~331 and ~394) to passwaitSecs: 0for instant status checksdatasetfield from structuredContent (no more preview items fromget-actor-run)statsandfinishedAtcontinue to work (kept in response)6. Cancellation in task mode
src/tools/core/call_actor_common.ts— thewaitForRunWithAbort()helper:waitForFinish()against the abort signalapifyClient.run(runId).abort()to stop the Actor on Apifynullto indicate cancellation (caller returns empty response)The abort signal chain: client cancels task → server's
abortController.abort()→ tool's abort signal fires →waitForRunWithAbortaborts the Apify run → task transitions to cancelled.7. README update
Update
README.md(~lines 208, 230) to reflect the new workflow:call-actorInternal repo impact
apify-mcp-server-internaldoes not import any of the changed functions (callActorGetDataset,buildActorResponseContent,callActorOutputSchema,getActorRunOutputSchema,fetchActorRunData). These are all internal implementation details not exported fromindex_internals.ts.The internal server imports only:
ActorsMcpServer,ApifyClient,HelperTools,getServerCard, tool registry functions, and utilities — none affected.Internal integration tests exercise
call-actorthrough the tool call layer and will see the new response shape. Assertion helpers in the internal test suite should be verified after the public package is updated.No changes needed in internal repo, but verify after merging.
Breaking changes
call-actorno longer blocks and returns dataset previewasyncparameter removedasync: true/falseget validation errorpreviewOutputparameter removedpreviewOutputget validation errorcallActorOutputSchemashape unchangedget-actor-rundrops dataset previewdatasetfield in response won't find itget-actor-runaddswaitSecswith default 10waitSecsnow wait up to 10s instead of returning instantly. Widget updated to passwaitSecs: 0.These are acceptable per project constraints: breaking changes are OK when they produce a simpler, clearer implementation.
Testing strategy
Unit tests
tests/unit/tools.structured_output_schemas.test.ts— add tests foractorRunOutputSchema, keepbuildEnrichedCallActorOutputSchemateststests/unit/tools.mode_contract.test.ts— description text changesgetHintForStatus()for all run statusesbuildCallActorStructuredContent()output formatwaitForRunWithAbort()— abort signal cancels wait and aborts runIntegration tests (in
tests/integration/suite.ts)storages/hintpreviewOutputtests (param removed)status: SUCCEEDED,hintcontains "get-actor-output"storages/hintwaitSecson get-actor-run — start actor, wait withwaitSecs: 30, assert terminal statuswaitSecs: 0returns immediately without waitingManual testing
waitSecs: 0, displays stats/finishedAtVerification checklist
npm run type-checkpassesnpm run lintpassesnpm run test:unitpasseswaitSecs: 0Follow-ups
Tracked in #587 (post-#582 roadmap):
get-actor-outputacceptingrunIdas alternative todatasetId— Phase 3 area