Execute Code
Use this action when a step needs custom logic that CEL cannot express — reshaping data, computing values, processing files, or calling an API with code. One action covers both languages: pick JavaScript or Python with the Language field.
Best for
- Transforming JSON from earlier steps into exactly the shape a later step needs
- Calculations, parsing, and validation beyond what CEL expressions can do
- Small integrations using
fetch(JavaScript) when the HTTP request action is not flexible enough - Document and file work with Python's bundled libraries — spreadsheets, PDFs, images, DOCX/PPTX
Choosing a language
| JavaScript | Python | |
|---|---|---|
| Runtime | Isolated V8 sandbox (no Node.js APIs) | Python 3.12 in a throwaway container |
| Network | fetch with SSRF protection | None — by design |
| Timeout (ms) | 100–30,000, default 5,000 | 1,000–120,000, default 30,000 |
| Memory (MB) | 8–256, default 128 | 128–2,048, default 512 |
| Libraries | Standard JavaScript built-ins | openpyxl, python-docx, python-pptx, pdfplumber, pypdf, pandas, Pillow, reportlab, markitdown — no pip at runtime |
Both languages share the same inputs and outputs: structured Input Data in, Input Files in, a structured result out, output files out, and logs in one unified shape.
Main fields
| Field | What it does |
|---|---|
| Language | javascript (default) or python |
| Code | The code to run. JavaScript is wrapped in an async IIFE, so you can use top-level await and end with return <value>. Python runs as a plain script; write the result to /work/out/output.json |
| Input Data | Optional CEL expression evaluated before execution. JavaScript sees the value as the input variable; Python sees it as the INPUT variable |
| Input Files | Optional CEL expression evaluating to an array of storage refs [{bucket, fullPath, name?}] — for example step(0).files or a file-typed workflow input. File contents are handed to the sandbox; the code never touches storage directly |
| Purpose | Optional label for what the code does, shown in logs |
| Timeout (ms) | Execution time limit in milliseconds, clamped per language (see the table above). Code that runs past it — including infinite loops — is terminated |
| Memory limit (MB) | Sandbox memory limit, clamped per language |
Files in, files out
Both directions share the same caps: at most 10 files, 20 MiB per file, 24 MiB total. File names must start with a letter or digit and may contain letters, digits, ., _, -, and spaces (up to 128 characters). The names input.json and output.json are reserved — they are the structured data channels.
Reading input files:
- JavaScript —
files.readText(name)returns the content as a text string (decoded as UTF-8);files.read(name)returns it as a base64 string for binary data - Python — files appear at
/work/in/<name>(read-only)
Writing output files:
- JavaScript —
files.writeText(name, text, {contentType?})for text;files.write(name, contentBase64, {contentType?})for binary.contentTypeis inferred from the extension when omitted - Python — write files to
/work/out/
For text work — CSV, JSON, Markdown — reach for readText / writeText and skip base64 entirely:
const rows = files.readText("data.csv").split("\n").map((line) => line.split(","));
files.writeText("summary.txt", `${rows.length} rows`);
The base64 pair is there for binary files (images, PDFs, spreadsheets), usually to pass bytes through unchanged or hand them to fetch.
Output files are uploaded to your company storage after the run and surface as storage refs in step(N).files. Later steps can chain them — for example, a Python step whose Input Files is step(0).files reads everything the previous code step produced.
The JavaScript sandbox
The code runs in an isolated V8 sandbox on a separate execution service. There is no access to Node.js APIs (require, process, file system) and no browser APIs — only standard JavaScript built-ins plus these bridges:
input— the evaluated Input Data value (nullwhen absent)console.log/info/warn/error— captured and returned as thelogsoutputfetch(url, options)— SSRF-guarded HTTP (private and internal addresses are blocked) returning{ok, status, statusText, body}, withbodyparsed as JSON when possible and returned as text otherwise. Response bodies are capped at 10 MiB — a larger response fails the step instead of being bufferedfiles.read/files.readText/files.write/files.writeText— see aboveatob(base64)/btoa(binaryString)— the standard base64 codec, for converting the valuesfiles.readandfiles.writedeal in.btoafollows the web rule of rejecting characters aboveU+00FF, so usefiles.writeTextfor UTF-8 text rather thanbtoa
There is no Buffer, no TextDecoder/TextEncoder, and no require — atob/btoa and the files text helpers are the supported way to move between bytes and strings.
The Python sandbox
The code runs as python3 job.py in a fresh container that is destroyed after the run:
- No network access — deliberate. Use the JavaScript language for HTTP calls
INPUT— the evaluated Input Data value, loaded from/work/in/input.json(Nonewhen absent)- Input files at
/work/in/(read-only), output files at/work/out/ - Write the structured step result as JSON to
/work/out/output.json— that is whatstep(N).resultreceives.print()goes to thelogsoutput, not to the result. A job that writes nooutput.jsongetsstep(N).result=null - Only the bundled libraries listed above are importable — there is no
pipat runtime - One Python job runs at a time; a busy runner fails the step with a retryable error (see below)
What later steps can use
step(N).result— the structured result (JavaScriptreturnvalue / Pythonoutput.json), capped at 2 MiB — write bulk data as files instead.nullwhen the code returned nothing / wrote nooutput.jsonstep(N).files— storage refs for the files the code wrote:[{name, bucket, fullPath, contentType, size}]step(N).logs— captured output as[{level, message}]entries (JavaScript console levels; Pythonstdout/stderr), up to 200 entries of 2,000 characters eachstep(N).executionTimeMs— how long the code ran
Earlier versions exposed the JavaScript return value as
step(N).data. It isstep(N).resultnow, for both languages.
Errors
| Code | Meaning |
|---|---|
code_execution_failed | The code threw an exception — check step(N).logs |
code_execution_timeout | The code ran past the timeout (both languages) |
code_executor_busy | The Python runner's single slot is occupied — retryable; running the step again is safe |
code_result_too_large | The result serializes to more than 2 MiB — return a small summary and write bulk data as files |
code_result_invalid_json | Python wrote /work/out/output.json, but it is not valid JSON |
code_input_files_invalid | Input Files did not evaluate to valid refs, exceeded the caps, or used a reserved name |
code_input_file_not_accessible | An input file was not found or is not accessible from this workspace |
code_input_file_unavailable | Transient storage failure while reading an input file — retryable |
python_unavailable | The Python language is not enabled in this environment |
Tips
- Always produce a result —
returnin JavaScript,output.jsonin Python — and keep it JSON-friendly. - Use
console.log/print()while building the step; thelogsoutput is the easiest way to debug. - Each
fetchcall is bounded by the step's timeout as well, so keep third-party calls well within the time budget. - Files are the right channel for anything big: the result is capped at 2 MiB, while files carry up to 24 MiB per run.