Skip to Content
πŸ›  BuildπŸ›  Action StepsCode editor

Code editor

The Code editor step runs Python inside your workflow and passes on whatever your code prints. Use it for the calculations, reshaping and business rules that the visual steps do not cover.

DockData Β· Dev toolsReturnswhatever your code prints

Look for it as β€œCode editor” in the dock. That is the label it carries there, under Tools β†’ Data β†’ Dev tools.

Keyboard shortcut: t+c

The step runs Python 3.13, with the standard library plus numpy and pandas available. The Language field reflects that and is fixed.

Setting it up

Add the step

Press t+c, or open Tools in the dock and select Code editor under Dev tools.

Write your Python

Reference earlier steps with {{ }} placeholders, on their own and without quotes. The section below covers the syntax.

The step passes on what you print, not what you return.

Run the workflow to see the result

Open the step’s Executions tab to read the output.

Reading data from earlier steps

Reference an earlier step by its number, the same as in any other field, and write the reference on its own and unquoted:

main.py
customer_name = {{ 2.customer_name }} order_total = {{ 2.total }} products = {{ 3.$full_result }}

Each reference arrives as a real Python value, not as text that gets pasted into your program. A JSON object becomes a dict, a list becomes a list, and true / false / null become True / False / None. Nothing needs quoting or escaping, and a customer called O'Brien cannot break your code.

{{ 3.$full_result }} gives you a whole step’s output as one value, which is the usual way to get a list of records to loop over.

β€œReference is inside a longer piece of text”

If the step fails with that message, this is the cause and the fix.

A reference has to be a value on its own. Written inside a longer piece of text it has no correct meaning, so the step refuses to run rather than guessing:

Rejected
print("Hello {{ 2.name }}")

Assign it first and build the text afterwards:

Works
name = {{ 2.name }} print(f"Hello {name}")

The same applies inside an f-string or any other quoted run, and to every kind of reference, {{ $var.KEY }} and {{ $secret.KEY }} included. Assign first, then use the variable.

Full placeholder syntax, including {{ $now }} and {{ $var.KEY }}, is documented in Variable Reference Syntax.

Available libraries

The step runs Python 3.13 with the full standard library, plus two data libraries. Import them as normal: there is nothing to install and nothing to declare.

The two data libraries

LibraryVersionFor
numpy2.5.1Numeric arrays and mathematics
pandas2.3.3Tables, grouping, joins and summarisation

Both versions are pinned rather than tracking the latest release, so the same program keeps producing the same result.

The standard library, by what you came to do

Everything in Python 3.13’s standard library imports, which is far more than most workflows need. These are the modules worth knowing about:

You want toImport
Read or write JSONjson
Do date and time arithmeticdatetime β€” and zoneinfo for time zones
Match or replace text patternsre
Read or write CSVcsv (with io.StringIO for text you already have)
Hash or sign somethinghashlib, hmac
Encode for an APIbase64, urllib.parse for query strings and escaping
Count, group or deduplicatecollections β€” Counter, defaultdict
Averages, medians, standard deviationstatistics
Money and exact decimalsdecimal β€” avoids the rounding errors of ordinary floats
Generate an IDuuid, secrets
Combine or chunk listsitertools, functools
Compress or unpack an archivegzip, zipfile, tarfile
main.py
import json import re from datetime import datetime, timedelta from zoneinfo import ZoneInfo from collections import Counter import pandas as pd

Finding what you need

Check the table above first: most of what people install a package for is already in the standard library. datetime, re, csv, hashlib and json between them cover the large majority of workflow code.

If it is a package for calling a service (requests being the usual one), that work belongs in a step rather than in code; see What to use instead below.

For anything genuinely missing, ask support. The library set is deliberately small so that runs stay reproducible, and additions are considered on request.

What it passes on

Print what you want to pass on. The step captures what your code writes to output. A bare return produces nothing downstream.

main.py
import json print(json.dumps({ "final_total": final_total, "discount_applied": discount, }))

What you printed sits under result.executionOutput:

{{ 4.result.executionOutput }}

Note the result. in the middle. This step nests its output one level deeper than most, so {{ 4.executionOutput }} finds nothing. Insert the reference from the data icon rather than typing it and you get the right path.

executionOutput is text, not an object. You cannot reach into it with {{ 4.result.executionOutput.final_total }}, because there is nothing to walk into. To use individual fields downstream, print JSON as above and put a Parse JSON step after this one.

print(my_dict) produces Python’s own formatting: single quotes, True, None. A Parse JSON step cannot read that. Use json.dumps() so the output is valid JSON. Class instances, lambdas and circular references cannot be serialised at all, so convert them to plain values first.

What to use instead

Your code runs on its own: everything it needs arrives through references, and everything it produces leaves through what it prints. Reaching out to the world is the job of the steps around it, not of the code. That keeps credentials out of a code box and every request visible in the run history.

A few things people reach for out of habit therefore do not apply here, and each has a step that does the job better:

HabitWhat happensUse instead
import requests to call an APIThe package is not available, and the network is unreachable regardlessAn HTTP Request step before this one to fetch, and one after it to send. It handles authentication, retries and errors for you.
Sending mail with smtplibNo connectionA Gmail, Outlook or SendGrid step from the dock’s Apps group.
Connecting to a database from codeNo connectionThe database step for that service, which keeps the credentials in your workspace.
open("out.csv", "w") beside your codeRead-only file systemWrite to /tmp for scratch space during the run, then print what matters. To keep a file, pass the contents to a Drive, S3 or email step.
Reading a file an earlier run wroteFileNotFoundErrorEvery run starts clean. Carry values between runs through the steps themselves, or store them in an app.
input() to ask for a valueEOFError β€” nobody is at a keyboardReference the value with {{ 2.field }}, or collect it with a Human Review step.

This is the design, not a gap to work around. A step that fetched its own data would hide that request from the run history, retry nothing when the API is down, and put an API key in a code box. Keeping the reaching-out in dedicated steps is what makes a failed run readable at three in the morning.

Limits

LimitValue
Run time150 seconds
Output kept100,000 characters
Memory1.5 GB
Network accessNone

Run time. A program that passes 150 seconds is stopped and the step fails. Filter large lists before expensive work, and check that loops terminate.

Output. Past 100,000 characters the result is stored truncated and marked with how many characters were dropped. A step printing a large dataset therefore passes on something that is no longer valid JSON. Print only what the next step needs. For a genuinely large result, print a summary and send the full data onward with a following HTTP Request or an app step such as Google Sheets.

Memory. Beyond 1.5 GB the program stops with a MemoryError. Process records in batches rather than loading everything at once.

Network. Your code cannot open connections, so urllib, socket and anything built on them will not reach a server. Fetch the data with an HTTP Request step before this one and read its result with {{ N.$full_result }}; to send something onward, print it and follow this step with an HTTP Request.

When a run fails

An uncaught exception fails the step and the branch stops. The error message carries what your program printed before it failed, including the traceback, so the step’s Executions tab tells you which line went wrong.

Guard optional data rather than assuming it is there β€” the chained .get() on the highlighted line is what stops a missing field failing the step:

main.py
payload = {{ 2.$full_result }} value = payload.get("response", {}).get("data", {}).get("value", "default")

Because a failed run stops the branch, there is no result carrying a flag to test: by the time a later step can read the output at all, the code ran. So the check worth adding on a critical path is not β€œdid it succeed?” but β€œis what it printed the shape I expected?”. Where the outcome drives money, deletion or anything else irreversible, follow the step with a Condition that asserts the shape and give the rest an error path.

Examples to copy

Work out a total

main.py
import json order_total = {{ 2.total }} discount = order_total * 0.1 if order_total > 100 else 0 final_total = order_total - discount print(json.dumps({ "final_total": round(final_total, 2), "discount_applied": round(discount, 2), }))

Score and rank a list

A workflow fetches product data and needs weighted scores before writing to a spreadsheet.

main.py
import json products = {{ 3.$full_result }} scored = [] for product in products: score = (product["rating"] * 0.6) + (product["reviews"] * 0.004) scored.append({ "name": product["name"], "score": round(score, 2), "tier": "premium" if score > 4.0 else "standard", }) scored.sort(key=lambda p: p["score"], reverse=True) print(json.dumps({"ranked_products": scored}))

Group and summarise with pandas

main.py
import json import pandas as pd orders = pd.DataFrame({{ 3.$full_result }}) by_region = ( orders.groupby("region")["amount"] .agg(["sum", "count"]) .round(2) .reset_index() ) print(json.dumps(by_region.to_dict(orient="records")))

Follow any of these with a Parse JSON step reading {{ 4.result.executionOutput }}, and the result becomes addressable field by field.

What’s Next?