Skip to main content
Updated Aug 5, 2026

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

JavaScriptPython
RuntimeIsolated V8 sandbox (no Node.js APIs)Python 3.12 in a throwaway container
Networkfetch with SSRF protectionNone — by design
Timeout (ms)100–30,000, default 5,0001,000–120,000, default 30,000
Memory (MB)8–256, default 128128–2,048, default 512
LibrariesStandard JavaScript built-insopenpyxl, 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

FieldWhat it does
Languagejavascript (default) or python
CodeThe 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 DataOptional CEL expression evaluated before execution. JavaScript sees the value as the input variable; Python sees it as the INPUT variable
Input FilesOptional 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
PurposeOptional 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. contentType is 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 (null when absent)
  • console.log/info/warn/error — captured and returned as the logs output
  • fetch(url, options) — SSRF-guarded HTTP (private and internal addresses are blocked) returning {ok, status, statusText, body}, with body parsed 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 buffered
  • files.read / files.readText / files.write / files.writeText — see above
  • atob(base64) / btoa(binaryString) — the standard base64 codec, for converting the values files.read and files.write deal in. btoa follows the web rule of rejecting characters above U+00FF, so use files.writeText for UTF-8 text rather than btoa

There is no Buffer, no TextDecoder/TextEncoder, and no requireatob/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 (None when 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 what step(N).result receives. print() goes to the logs output, not to the result. A job that writes no output.json gets step(N).result = null
  • Only the bundled libraries listed above are importable — there is no pip at 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 (JavaScript return value / Python output.json), capped at 2 MiB — write bulk data as files instead. null when the code returned nothing / wrote no output.json
  • step(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; Python stdout / stderr), up to 200 entries of 2,000 characters each
  • step(N).executionTimeMs — how long the code ran

Earlier versions exposed the JavaScript return value as step(N).data. It is step(N).result now, for both languages.

Errors

CodeMeaning
code_execution_failedThe code threw an exception — check step(N).logs
code_execution_timeoutThe code ran past the timeout (both languages)
code_executor_busyThe Python runner's single slot is occupied — retryable; running the step again is safe
code_result_too_largeThe result serializes to more than 2 MiB — return a small summary and write bulk data as files
code_result_invalid_jsonPython wrote /work/out/output.json, but it is not valid JSON
code_input_files_invalidInput Files did not evaluate to valid refs, exceeded the caps, or used a reserved name
code_input_file_not_accessibleAn input file was not found or is not accessible from this workspace
code_input_file_unavailableTransient storage failure while reading an input file — retryable
python_unavailableThe Python language is not enabled in this environment

Tips

  • Always produce a result — return in JavaScript, output.json in Python — and keep it JSON-friendly.
  • Use console.log / print() while building the step; the logs output is the easiest way to debug.
  • Each fetch call 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.