Python Code Runner - pip, pandas & Dataset In/Out
Pricing
from $4.00 / 1,000 code runs
Python Code Runner - pip, pandas & Dataset In/Out
Run your Python main() on rows from a Dataset, JSON or n8n in one API call. pip packages from a requirements field, pandas and openpyxl preinstalled, encrypted secrets as environment variables, rows back with per-item status.
Pricing
from $4.00 / 1,000 code runs
Rating
0.0
(0)
Developer
Vadim Bezrukov
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Run your own Python main() on rows in one API call and get rows back. Send the items of an n8n, Make or Zapier step as rows, or point datasetId at another Actor's Dataset; pandas, numpy and openpyxl are already installed, extra packages come from a requirements field, API keys go in encrypted secrets that your code reads as environment variables. Whatever main returns lands in the run's Dataset in your own shape, and a failure arrives as an explicit status row, never as an empty success.
Built for automation steps that need real Python: an n8n Cloud Code node that refuses import pandas or import re, a Pipedream Python step that has to move before Pipedream Workflows shut down on 2027-03-31, or an agent asked to "run this Python with pandas".
Latest notes are in the changelog.
What the first run returns
The Console prefill sends three order rows and this code:
import pandas as pddef main(rows, ctx):df = pd.DataFrame(rows)df["total"] = df["price"] * df["qty"]return df.sort_values("total", ascending=False)
The default Dataset gets three rows, sorted, each with the new total column, and RUN_SUMMARY says SUCCESS:
[{"sku": "C-3", "price": 99.0, "qty": 1, "total": 99.0},{"sku": "B-2", "price": 3.2, "qty": 20, "total": 64.0},{"sku": "A-1", "price": 12.5, "qty": 4, "total": 50.0}]
That run costs about $0.0043: one code-run event ($0.004), which includes the first 30 GB-seconds of compute, plus the $0.0003 start event.
n8n in one node
Add an HTTP Request node after the node that holds your items:
- Method
POST, URLhttps://api.apify.com/v2/acts/automa-flow~python-code-runner/run-sync-get-dataset-items - Authentication: a Header Auth credential with
Authorization=Bearer <your Apify API token>. Keep the token in the credential, never in the URL. - Body (JSON), with the previous node's items as
rows:
{"code": "import pandas as pd\n\ndef main(rows, ctx):\n df = pd.DataFrame(rows)\n return df.groupby('customer', as_index=False)['amount'].sum()\n","rows": {{ JSON.stringify($input.all().map(item => item.json)) }}}
The response is the list of rows main returned, ready for the next node (Google Sheets, Airtable, a database, an LLM step). The official Apify node for n8n works the same way: pick this Actor, paste the same input and read the run's Dataset items. Set failRunOnError to true if the workflow should stop on a code error.
Why use it
- Rows in, rows out, one call. Inline
rows, a Dataset (datasetId) or an Actor-to-Actor integration go in; the rows your code returns come out in your own columns, with no envelope to unwrap. - pip packages from a field. pandas and 15 other common packages are preinstalled, so most runs install nothing. Add more with
requirements, one PyPI line each. - Secrets as environment variables. Keys go in
secrets(encrypted in the input), your code readsos.environ["API_KEY"], and the values are removed from logs, status rows andRUN_SUMMARY. The code text itself is never logged, only its SHA-256. - A status for every item. In
eachItemmode one bad row never stops the others;RUN_SUMMARYlists the input indexes to resend. - Bounded. Code timeout, output row cap, row size cap, log cap and
maxTotalChargeUsdall hold, so an agent or a schedule cannot run away with a bill. - Made to be called again. Every n8n or Make execution, schedule tick or scraper run is one call with the same code and new rows, so the Python step lives inside the workflow instead of on someone's laptop.
Call modes
callMode | Calls | Use it for |
|---|---|---|
allItems (default) | main(rows, ctx) once, with the list of all input rows | pandas reshaping, grouping, joins, one summary |
eachItem | main(row, ctx) once per input row | per-row scoring, parsing or API calls where one failure must not stop the rest |
The names follow n8n's "Run Once for All Items" and "Run Once for Each Item". main may be async, may be a generator, and may take fewer parameters (main(), main(rows)). With no rows and no datasetId, main([], ctx) runs once as a script and reads its settings from ctx.input.
ctx holds ctx.input (the input object), ctx.call_mode, ctx.item_index (the row index in eachItem mode) and ctx.save_file(name, data, content_type=None).
Input
| Field | Default | Notes |
|---|---|---|
code | prefill | Python 3.12 source that defines main, up to 200,000 characters |
callMode | allItems | allItems or eachItem |
rows | prefill | Up to 10,000 JSON objects. Use rows or datasetId, not both |
datasetId | none | Dataset to read; in an integration use {{resource.defaultDatasetId}} |
datasetOffset / maxRows | 0 / 10,000 | Range of Dataset rows, up to 100,000 per run |
fields | all | Up to 50 top-level fields to read from each Dataset row |
input | {} | Parameters your code reads as ctx.input |
requirements | empty | Extra PyPI packages, one per line, up to 30 lines |
secrets | none | Up to 20 environment variables, encrypted |
timeoutSeconds | 60 | 1-900 s for starting Python and running main |
installTimeoutSeconds | 120 | 10-300 s for the install |
maxOutputRows | 10,000 | 1-100,000; rows above it are dropped and counted |
maxRowBytes | 1 MiB | 1 KiB to 4 MiB per returned row |
maxLogBytes / logUserOutput | 256 KiB / on | How much print output reaches the run log |
statusRows | failures | failures, all or none |
failRunOnError | off | End the run FAILED when the status is not SUCCESS or NO_ROWS |
maxInputBytes | 64 MiB | 1-256 MiB of input passed to main |
Run memory is 1,024 MB by default (1,024 to 4,096 MB), the run timeout 1,800 s. A synchronous API call (run-sync-get-dataset-items) waits at most 300 s, so start longer jobs asynchronously and read the Dataset when the run finishes.
Return value rules
main returns | Rows written |
|---|---|
None | none (NO_ROWS) |
a dict | one row |
| a list, tuple, generator or async generator of dicts | those rows |
a pandas DataFrame | one row per record (to_dict(orient="records")) |
a pandas Series of dicts | those dicts |
Values convert the same way every time: NaN, NaT and infinities become null; numpy and pandas numbers and booleans become JSON numbers and booleans; datetime, date and Timestamp become ISO 8601 text (Z for timezone-aware values, converted to UTC); Decimal becomes a string; sets become sorted lists; numpy arrays become lists. Anything else (bytes, custom objects, a list member that is not a dict) fails that unit with OUTPUT_INVALID and names the JSON path of the first bad value, for example $[3].price. A row larger than maxRowBytes fails the same way. The key _status is reserved for status rows, so a returned row that has it is refused.
Packages
Preinstalled in the code's own environment (Python 3.12.13):
pandas 3.0.6, numpy 2.5.3, openpyxl 3.1.5, xlrd 2.0.2, python-dateutil 2.9.0.post0, tzdata 2026.5, requests 2.34.2, httpx 0.28.1, beautifulsoup4 4.15.0, lxml 6.1.3, pypdf 6.19.0, python-docx 1.2.0, Pillow 12.3.0, PyYAML 6.0.3, Jinja2 3.1.6, rapidfuzz 3.14.6.
requirements takes the requirements.txt lines you know: a name, optional extras, an optional version and an optional environment marker, such as tabulate==0.9.0, polars>=1,<2 or requests[socks]. Blank lines and # comments are fine. A line that an installed version already satisfies is skipped and reported in RUN_SUMMARY.packages.skipped. Asking for another version of a preinstalled package (for example pandas==2.2.3) replaces it for that run only; the Actor's own runtime is separate and cannot break.
Refused before anything is installed, with the line number: options (-r, -c, -e, --index-url, --extra-index-url, --find-links, --hash), direct references (name @ url), URLs, git+ and other VCS links, local paths and archive files, and wildcards in a name. Packages come from PyPI only, and only as wheels for Linux x86_64 and CPython 3.12: a source-only package ends PACKAGE_INSTALL_FAILED instead of compiling code at install time. Pick packages you trust; a misspelled name can be someone else's package.
Secrets
secrets is an object of environment variables, for example {"API_KEY": "...", "SHEET_ID": "..."}. Keys use A-Z, 0-9 and _, start with a letter and cannot start with APIFY_, ACTOR_, PYTHON, LD_, UV_ or PIP_ or be PATH, HOME, TZ, LANG or LC_ALL. Values up to 8 KB. Before anything reaches the run log, a status row or RUN_SUMMARY, every secret value of 6 or more characters, and its URL-encoded and base64 forms, is replaced with [REDACTED:<KEY>]. Shorter values are left as they are. Rows and files your code returns are your data and are stored exactly as returned, so do not return a secret.
Output rows and status rows
Your rows go to the default Dataset as returned. Status rows carry _status and sit in the same Dataset:
{"_status": "CODE_ERROR","_unit": "item","_input_index": 2,"_error_type": "KeyError","_error": "'price'","_error_line": 4,"_stderr_tail": "Traceback (most recent call last): ...","_rows_returned": 0,"_duration_ms": 3,"_finished_at": "2026-10-20T12:00:00Z","_schema_version": 1}
With statusRows: failures (the default) a status row is written for every failed unit, and one row when the run wrote no rows of its own, so a run never ends with an empty Dataset and no explanation. That includes FAILED runs: rejected input writes one FAILED row whose _error_type names the reason, such as INVALID_INPUT. all adds a row for every unit, none writes none. _unit is call for the single allItems call and item for one input row; _input_index is the row's position, and for Dataset input its absolute offset. The results view shows everything; the status view shows only the status columns. Apify views select columns and do not filter rows, so filter on _status in the next step when you need only one kind.
Statuses: SUCCESS, NO_ROWS, CODE_ERROR, OUTPUT_INVALID, TIMEOUT, MEMORY_LIMIT, NOT_RUN (an eachItem row that was never reached because the run stopped), plus the run-level PACKAGE_INSTALL_FAILED, BUDGET_EXCEEDED, INTERRUPTED, INPUT_UNAVAILABLE and FAILED.
Files
ctx.save_file("report.xlsx", data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") stores bytes or str in the run's Key-Value Store as files-report.xlsx. Up to 10 files and 50 MiB per run; names use letters, digits, ., _ and -. Over a limit, save_file raises ValueError inside your code so you can see why. RUN_SUMMARY.files lists each key, size, SHA-256, content type and record URL.
RUN_SUMMARY
Read it first. It holds the run status, callMode, the input source and counts, Dataset paging (datasetItemsTotal, nextDatasetOffset), counts per item status, rows written and dropped by the cap, packages (requested, skipped, installed with versions, download bytes), install and execution seconds, billed GB-seconds, charged events, billingStatus, codeSha256 and codeBytes (never the code), the first error with type, line and redacted message, failedInputIndexes (up to 1,000) to resend, and nextAction.
For a PARTIAL run, send only the rows whose indexes are in failedInputIndexes after fixing the code. When nextDatasetOffset is set, run again with that datasetOffset for the rest.
Pricing
Platform usage is included in the prices: you pay only these events.
| Event | Price | When |
|---|---|---|
| Actor start | $0.0003 per GB of memory | every run |
code-run | $0.004 | once per run in which your code started and finished, including a code error, a timeout or a memory or output limit; includes the first 30 GB-seconds |
extra-gb-minute | $0.01 | each started GB-minute of install plus execution time beyond the included 30 GB-seconds |
packages-installed | $0.004 | once per run that installed at least one package that was not preinstalled |
result-rows-100 | $0.0015 | each started 100 returned rows beyond the first 100 of a run |
Not charged: invalid or unreadable input, a failed install, a run that maxTotalChargeUsd stopped before the code started, an interrupted run, status rows, saved files and rows above maxOutputRows. A code error is charged like a success because the compute was used.
| Example | Bill |
|---|---|
| The prefill (three rows, pandas, 1 GB) | about $0.0043 |
| An n8n workflow calling it 30 times a day with 500 rows back and a 5 s pandas step | about $0.31 a day ($0.0103 a call) |
| A daily 3-minute job at 2 GB (360 GB-seconds) | about $0.065 |
| 20,000 returned rows in one run | adds 199 x $0.0015 = $0.30 |
maxTotalChargeUsd is respected before the code starts: the Actor computes the largest bill the run could reach, shrinks the code timeout and then the row cap to fit, and says so in RUN_SUMMARY.notes. If even one code-run does not fit, the run ends BUDGET_EXCEEDED without running anything.
Failure semantics
A problem in your code is an outcome, not a failed run: the run ends SUCCEEDED with status rows and a RUN_SUMMARY that names the error. The run itself fails only when the Actor cannot do its job: the input is invalid (including a refused requirements line or a Dataset id that sits only inside an integration payload), the input Dataset cannot be read (INPUT_UNAVAILABLE, nothing charged, never treated as an empty input), storage cannot be written, a charge did not finish cleanly, or the image is broken. An empty Dataset is not a failure: allItems calls main([], ctx) and eachItem ends NO_ROWS without a charge.
failRunOnError: true makes the run end FAILED after the rows and RUN_SUMMARY are stored whenever the status is not SUCCESS or NO_ROWS, which is what an n8n HTTP Request node or a Make module needs to stop the scenario.
If the platform restarts a run while your code is running, the code is not run again (it may already have sent an email or written to an API): the run ends INTERRUPTED, rows already written stay, and nothing more is charged. Start a new run for the rest. User code is never retried and a failed install is never retried.
Security model and its limits
Your code runs as a separate unprivileged user in its own process with a fresh environment: the run's Apify token and proxy password are not in that environment and the process cannot read the Actor's memory or environment. Limits on memory (run memory minus 384 MiB), processes (128), open files (512), file size (1 GiB) and CPU time hold, and at the timeout every process the code started is stopped, including ones that detached. The Actor's own runtime and the runner are read-only to your code. Packages go into the code's own environment, never into the Actor's.
This is not a virtual machine. The code and the installer have outbound network access, because calling APIs is a core use; nothing isolates that traffic. You are responsible for the code you run, the services it contacts and the packages you install. Do not run code you have not reviewed. Your rows are never logged and never sent anywhere except this run's Dataset; your print output is logged when logUserOutput is on, redacted and capped.
Moving a Pipedream Python step
| Pipedream | Here |
|---|---|
def handler(pd: "pipedream") | def main(rows, ctx) |
pd.steps["trigger"]["event"] and earlier step exports | rows (the items you send) and ctx.input (fixed parameters) |
return value / pd.export | the value main returns becomes the rows |
| environment variables | secrets |
| packages imported at the top | preinstalled, or one line each in requirements |
API, Make, integrations and MCP
- API:
POST https://api.apify.com/v2/acts/automa-flow~python-code-runner/run-sync-get-dataset-itemsreturns the rows directly (up to 300 s);POST .../runsstarts an asynchronous run. Send the token in theAuthorization: Bearerheader. - Make: an HTTP "Make a request" module with the same URL, method and JSON body, and the token in a header.
- After a scraper: on the scraper's Integrations tab, connect this Actor (or a saved Task of it) and set
datasetIdto{{resource.defaultDatasetId}}, so your code runs over each new Dataset. A dataset id that sits only inside the integration'spayloadis not read; the run fails and says so. - Schedules: save a Task with your code and
inputparameters (for example a date window) and put it on a schedule.
Agents can find the Actor through Apify MCP search; running it uses your Apify account, and timeoutSeconds, maxOutputRows and maxTotalChargeUsd bound each call. RUN_SUMMARY gives an agent the outcome without reading every row. For example, ask an agent: "Use automa-flow/python-code-runner to group these 200 order rows by customer with pandas, return the totals and stop at $0.05." It sends the rows and the code, then reads the status and the grouped rows back.
Direct MCP endpoint:
https://mcp.apify.com?tools=automa-flow/python-code-runner
If a run fails or looks wrong, open an Issue and include the run URL. Apify shares that run with us automatically, so there is no need to paste your code, data or keys.
Technical details
- Python 3.12.13 in
apify/actor-python:3.12; packages installed withuvfromhttps://pypi.org/simple, wheels only, no user configuration files read. - The code runs from a fresh
user_main.py, so tracebacks point at your own line numbers. - Inline rows are spooled to a file the code cannot read, then streamed to the code process; Dataset rows are read page by page, empty rows keep their place, and the read stops at
maxInputByteswith a next offset.