To export an Apify Actor's results, start its run, save the returned run ID, wait for completion and read defaultDatasetId. Request the dataset's items as CSV or JSON. If a finished run already exists, reuse its Dataset ID: downloading those saved results does not require another Actor run.
You can do this manually in Apify Console, from your application using the API, or through your AI agent with Apify MCP. The same three things matter in every route: correct input, recorded run identity and a complete result export.
Choose Console, API or an AI-agent workflow
| Route | Useful when | What you manage |
|---|---|---|
| Apify Console | You want an editable form and a first CSV | Input, Run options, reports and download. |
| Apify API | Your application needs repeatable processing | Authentication, run ID, status, storage and retries. |
| Apify MCP | Your compatible AI agent should coordinate the workflow | Actor choice, scope, spending cap and output requirements. |
For a first manual run, open the Actor, choose Try for free, sign in and edit Input in Console. Review Pricing, set a small result limit and spending cap, then start. Inspect Output and the source-coverage report before choosing CSV or JSON from the dataset export controls.
Build input for the specific Actor
There is no universal scraper payload. Twitter followers, website contacts and Zillow details accept different inputs and return different row types. Inspect the selected Actor's current Input, Output and README before copying an example.

The figure shows the read-only reference for the Twitter Followers Scraper. In this guide, it is the concrete example: request five follower connections for a public username, with username output explicitly selected. For the full collection workflow, see exporting Twitter followers and following.
{
"userNameList": ["nasa"],
"getFollowers": true,
"maxFollowers": 5,
"getFollowing": false,
"outputMode": "usernames"
}The follower limit belongs in this Actor's input. An API charge cap is a separate run option. A technical endpoint such as https://api.apify.com/v2 should remain unchanged; referral parameters belong on editorial Apify links.
Use the right identifier at each step
| Action | Endpoint under api.apify.com | Keep |
|---|---|---|
| Start the Actor | POST /v2/actors/{actorId}/runs | data.id, the run ID. |
| Check that run | GET /v2/actor-runs/{runId} | Status, defaultDatasetId and defaultKeyValueStoreId. |
| Download rows | GET /v2/datasets/{datasetId}/items | The exported response body. |
Use owner~actor-name in the start URL, such as x_guru~twitter-follower-scraper. The run ID identifies one execution. The Dataset ID identifies saved rows. Do not put a run ID into the Dataset endpoint.
Send your Apify token in the Authorization bearer header and keep it in an environment variable or your application's credential store. Follow the Actor's documented output if it uses a named dataset or another storage location instead of the default dataset.
Run a small Python example and save its CSV
Install requests and set APIFY_TOKEN. This example starts a chargeable Actor run with a small PPE spending cap. It writes the run ID to actor-run.json before polling. To resume checking an existing execution, set APIFY_RUN_ID; that skips the start request.
import json
import os
import time
from pathlib import Path
import requests
base = "https://api.apify.com/v2"
session = requests.Session()
session.headers["Authorization"] = (
f"Bearer {os.environ['APIFY_TOKEN']}"
)
payload = {
"userNameList": ["nasa"],
"getFollowers": True,
"maxFollowers": 5,
"getFollowing": False,
"outputMode": "usernames",
}
run_id = os.environ.get("APIFY_RUN_ID")
if not run_id:
start = session.post(
f"{base}/actors/"
"x_guru~twitter-follower-scraper/runs",
params={"maxTotalChargeUsd": 0.10},
json=payload,
timeout=20,
)
start.raise_for_status()
run_id = start.json()["data"]["id"]
Path("actor-run.json").write_text(
json.dumps({"runId": run_id}),
encoding="utf-8",
)
print("Run ID:", run_id)
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
response = session.get(
f"{base}/actor-runs/{run_id}", timeout=20
)
response.raise_for_status()
run = response.json()["data"]
if run["status"] == "SUCCEEDED":
break
if run["status"] in {
"FAILED", "ABORTED", "TIMED-OUT"
}:
raise RuntimeError(
f"Inspect run {run_id}: {run['status']}"
)
time.sleep(3)
else:
raise TimeoutError(
f"Check existing run {run_id}; do not restart"
)
export = session.get(
f"{base}/datasets/"
f"{run['defaultDatasetId']}/items",
params={"format": "csv", "bom": 1},
timeout=30,
)
export.raise_for_status()
Path("actor-results.csv").write_bytes(export.content)The 180-second client wait is not a guarantee that the run will finish in that time. If it expires, inspect the existing run using the recorded ID. A client timeout does not automatically stop an Actor. Review logs and source coverage before treating a successful dataset as complete.
Export all JSON pages without starting again
Use JSON for nested objects, joins or batch imports. The reader below uses the authenticated Session from the earlier example and assumes a completed, unchanged dataset. It deliberately leaves row-skipping transformations off. Persist each completed offset only after the destination accepts its rows.
def read_dataset_pages(session, dataset_id, limit=1000):
url = (
"https://api.apify.com/v2/datasets/"
f"{dataset_id}/items"
)
offset = 0
while True:
response = session.get(
url,
params={
"format": "json",
"offset": offset,
"limit": limit,
},
timeout=30,
)
response.raise_for_status()
rows = response.json()
if not isinstance(rows, list):
raise ValueError("Expected Dataset items list")
if not rows:
break
yield offset, rows
offset += len(rows)
total = response.headers.get(
"X-Apify-Pagination-Total"
)
if total is not None and offset >= int(total):
breakFor 2,130 saved items with pages of 1,000, the expected offsets are 0, 1,000 and 2,000. Increase by the actual returned count. If you later add clean, skip-empty or other transformations, review the endpoint's pagination semantics instead of assuming the same loop still applies.
Dataset pagination reads already saved rows. Actor continuation collects additional source results. They are different operations: use that Actor's documented PAGINATION.nextInput when collection itself is incomplete, and keep each new run and dataset in your ledger.
Let your AI agent coordinate the run through Apify MCP
Add https://mcp.apify.com to a compatible MCP client and authorize your Apify account. Follow the official connection guide. You can ask for the same bounded workflow in ordinary language.
Use Apify MCP. Inspect the current schema for
x_guru/twitter-follower-scraper.
Collect five follower connections for nasa.
Set outputMode to usernames, getFollowers true,
maxFollowers 5 and getFollowing false.
Set callOptions.maxTotalChargeUsd to 0.10.
Record the run ID and wait for completion.
Retrieve every page of the default dataset.
Export CSV and report actual row count,
source coverage and any unfinished work.
If the run exists, reuse it. Do not start
another paid run just to download its rows.fetch-actor-details provides the current contract. call-actor starts the execution and returns status and storage IDs; it does not itself return the complete dataset. Use get-actor-run, get-dataset-items with limit/offset and get-key-value-store-record for the remaining steps.
Use callOptions.maxTotalChargeUsd for the MCP run cap rather than inserting it into Actor input. A compatible client can also expose an Actor as a dedicated tool. The official MCP server repository documents available tools and their behavior.
Handle partial runs, empty output and retries
| Situation | Next action |
|---|---|
| Start request timed out before returning an ID | Check run history against time and input before repeating the POST; it may already have started. |
| Status GET failed temporarily | Keep the same run ID and retry with a delay; respect Retry-After. |
| Run failed, timed out or was aborted | Inspect logs and any saved output. Label retained rows partial. |
| Dataset GET returned 401 or 403 | Check credentials and account access to that storage. |
| Successful run has no rows | Read the Actor's source and billing reports; zero matches and source failure are different outcomes. |
Production integrations should keep a run ledger, bounded retry policy and destination upserts. The Python example demonstrates the basic flow; it does not implement a durable job queue. If you later use n8n, the same sequence is POST → Wait/status loop → dataset GET. n8n is optional; direct API code is sufficient.
See the official references for starting an Actor, reading run status and Dataset exports. Choose your next source in the Actor directory, or follow the two-Actor follower-to-email workflow.



