Skip to main content
Updated Nov 1, 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 (either language) 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 protectionfetch with SSRF protection (host-mediated)
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-ins and published company modulesopenpyxl, 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 workflow.step(0).files or a file-typed workflow input. File contents are handed to the sandbox; the code never touches storage directly
LibrariesPublished company libraries pinned to exact versions for this code step and language
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

Reusable company libraries​

Logic you need in more than one step can live in a company library: a JavaScript or Python module published as numbered versions that never change. A step pins an exact version of each library it uses, in the Libraries box above the code, and keeps that version until you update it.

  • Add a library pins a published library in the step's language. When a newer version exists, Update to vN shows what changes before you move to it.
  • Open a library's row to see its exports and parameters, and insert a call at the cursor.
  • Changing the step's Language removes its pinned libraries.

A JavaScript step calls a pinned library through autotalk_libraries:

return autotalk_libraries.greetings.greet(input.name);

A Python step imports it and writes its result as usual:

import json
from autotalk_libraries import greetings

with open("/work/out/output.json", "w") as output:
json.dump(greetings.greet(INPUT["name"]), output)

A JavaScript step reaches only the libraries pinned on it, not their own imports. The MCP run_code tool accepts the same exact pins as codeLibraries: [{libraryId, releaseId}], and get_code_library_reference finds them. To write, test and publish libraries, and for their limits and permissions, see Libraries.

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 workflow.step(N).files. Later steps can chain them — for example, a Python step whose Input Files is workflow.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 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:

  • INPUT — the evaluated Input Data value, loaded from /work/in/input.json (None when absent)
  • fetch(url, method="GET", headers=None, body=None) — the same SSRF-guarded HTTP as the JavaScript lane, provided by the platform (the container itself has no raw network; each request is mediated by the host). Returns a dict {"ok", "status", "statusText", "body"}, with body parsed as JSON when possible and returned as text otherwise; response bodies are capped at 10 MiB. It raises FetchError when the request cannot be made (a blocked or unreachable target). A non-2xx response is not an error — it returns with ok=False, just like the JavaScript lane. Do not rely on requests or urllib for outbound calls — use fetch()
  • 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 workflow.step(N).result receives. print() goes to the logs output, not to the result. A job that writes no output.json gets workflow.step(N).result = null
  • Standard-library modules, the bundled packages listed above, and pinned company libraries 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)

Fetching data and shaping it with the bundled libraries:

import json
resp = fetch("https://api.example.com/report", headers={"accept": "application/json"})
if not resp["ok"]:
raise FetchError(f"upstream returned {resp['status']}")
rows = resp["body"]["items"]
with open("/work/out/output.json", "w") as f:
json.dump({"count": len(rows)}, f)

What later steps can use​

  • workflow.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
  • workflow.step(N).files — storage refs for the files the code wrote: [{name, bucket, fullPath, contentType, size}]
  • workflow.step(N).logs — captured output as [{level, message}] entries (JavaScript console levels; Python stdout / stderr), up to 200 entries of 2,000 characters each
  • workflow.step(N).executionTimeMs — how long the code ran

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

Because this step publishes result and no data, workflow.stepData and workflow.stepHasContent resolve inside result: workflow.stepData(N, "count") reads workflow.step(N).result.count, which is usually what you want. The three siblings at the step root are invisible to them — read those with maps.get(workflow.step(N), "files"), not workflow.stepHasContent(N, "files"), which would miss silently while workflow.stepOk(N) stays true. This holds even when the code returned nothing, because result is then null. See Actions that publish at the step root.

Errors​

CodeMeaning
code_execution_failedThe code threw an exception — check workflow.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.