Python Code Runner - pip, pandas & Dataset In/Out avatar

Python Code Runner - pip, pandas & Dataset In/Out

Pricing

from $4.00 / 1,000 code runs

Go to Apify Store
Python Code Runner - pip, pandas & Dataset In/Out

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

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

2 days ago

Last modified

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 pd
def 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, URL https://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 reads os.environ["API_KEY"], and the values are removed from logs, status rows and RUN_SUMMARY. The code text itself is never logged, only its SHA-256.
  • A status for every item. In eachItem mode one bad row never stops the others; RUN_SUMMARY lists the input indexes to resend.
  • Bounded. Code timeout, output row cap, row size cap, log cap and maxTotalChargeUsd all 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

callModeCallsUse it for
allItems (default)main(rows, ctx) once, with the list of all input rowspandas reshaping, grouping, joins, one summary
eachItemmain(row, ctx) once per input rowper-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

FieldDefaultNotes
codeprefillPython 3.12 source that defines main, up to 200,000 characters
callModeallItemsallItems or eachItem
rowsprefillUp to 10,000 JSON objects. Use rows or datasetId, not both
datasetIdnoneDataset to read; in an integration use {{resource.defaultDatasetId}}
datasetOffset / maxRows0 / 10,000Range of Dataset rows, up to 100,000 per run
fieldsallUp to 50 top-level fields to read from each Dataset row
input{}Parameters your code reads as ctx.input
requirementsemptyExtra PyPI packages, one per line, up to 30 lines
secretsnoneUp to 20 environment variables, encrypted
timeoutSeconds601-900 s for starting Python and running main
installTimeoutSeconds12010-300 s for the install
maxOutputRows10,0001-100,000; rows above it are dropped and counted
maxRowBytes1 MiB1 KiB to 4 MiB per returned row
maxLogBytes / logUserOutput256 KiB / onHow much print output reaches the run log
statusRowsfailuresfailures, all or none
failRunOnErroroffEnd the run FAILED when the status is not SUCCESS or NO_ROWS
maxInputBytes64 MiB1-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 returnsRows written
Nonenone (NO_ROWS)
a dictone row
a list, tuple, generator or async generator of dictsthose rows
a pandas DataFrameone row per record (to_dict(orient="records"))
a pandas Series of dictsthose 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.

EventPriceWhen
Actor start$0.0003 per GB of memoryevery run
code-run$0.004once 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.01each started GB-minute of install plus execution time beyond the included 30 GB-seconds
packages-installed$0.004once per run that installed at least one package that was not preinstalled
result-rows-100$0.0015each 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.

ExampleBill
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 stepabout $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 runadds 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

PipedreamHere
def handler(pd: "pipedream")def main(rows, ctx)
pd.steps["trigger"]["event"] and earlier step exportsrows (the items you send) and ctx.input (fixed parameters)
return value / pd.exportthe value main returns becomes the rows
environment variablessecrets
packages imported at the toppreinstalled, 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-items returns the rows directly (up to 300 s); POST .../runs starts an asynchronous run. Send the token in the Authorization: Bearer header.
  • 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 datasetId to {{resource.defaultDatasetId}}, so your code runs over each new Dataset. A dataset id that sits only inside the integration's payload is not read; the run fails and says so.
  • Schedules: save a Task with your code and input parameters (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 with uv from https://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 maxInputBytes with a next offset.