# Code editor

> Run Python inside a workflow: how to read earlier steps, which libraries are available, what the step passes on, and the limits it runs under.

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.

> Dock: Data · Dev tools · Returns: whatever 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.

### Print what the next step needs

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**:

```python filename="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:

```python filename="Rejected"
print("Hello {{ 2.name }}")
```

Assign it first and build the text afterwards:

```python filename="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](/reference/variable-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

| Library    | Version | For                                       |
| ---------- | ------- | ----------------------------------------- |
| **numpy**  | 2.5.1   | Numeric arrays and mathematics            |
| **pandas** | 2.3.3   | Tables, 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 to                           | Import                                                    |
| ------------------------------------- | --------------------------------------------------------- |
| Read or write JSON                    | `json`                                                    |
| Do date and time arithmetic           | `datetime` — and `zoneinfo` for time zones                |
| Match or replace text patterns        | `re`                                                      |
| Read or write CSV                     | `csv` (with `io.StringIO` for text you already have)      |
| Hash or sign something                | `hashlib`, `hmac`                                         |
| Encode for an API                     | `base64`, `urllib.parse` for query strings and escaping   |
| Count, group or deduplicate           | `collections` — `Counter`, `defaultdict`                  |
| Averages, medians, standard deviation | `statistics`                                              |
| Money and exact decimals              | `decimal` — avoids the rounding errors of ordinary floats |
| Generate an ID                        | `uuid`, `secrets`                                         |
| Combine or chunk lists                | `itertools`, `functools`                                  |
| Compress or unpack an archive         | `gzip`, `zipfile`, `tarfile`                              |

```python filename="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](#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.

```python filename="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](/build/action-steps/parse-json) step after this one.

### Print JSON, not Python objects

`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:

| Habit                                   | What happens                                                            | Use instead                                                                                                                                                         |
| --------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `import requests` to call an API        | The package is not available, and the network is unreachable regardless | An [HTTP Request](/build/action-steps/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 `smtplib`             | No connection                                                           | A Gmail, Outlook or SendGrid step from the dock's **Apps** group.                                                                                                   |
| Connecting to a database from code      | No connection                                                           | The database step for that service, which keeps the credentials in your workspace.                                                                                  |
| `open("out.csv", "w")` beside your code | `Read-only file system`                                                 | Write 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 wrote     | `FileNotFoundError`                                                     | Every run starts clean. Carry values between runs through the steps themselves, or store them in an app.                                                            |
| `input()` to ask for a value            | `EOFError` — nobody is at a keyboard                                    | Reference the value with `{{ 2.field }}`, or collect it with a [Human Review](/build/action-steps/user-approval) 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

| Limit          | Value              |
| -------------- | ------------------ |
| Run time       | 150 seconds        |
| Output kept    | 100,000 characters |
| Memory         | 1.5 GB             |
| Network access | None               |

**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](/build/action-steps/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](/build/action-steps/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:

```python filename="main.py" {2}
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](/build/action-steps/conditions) that asserts the shape and give the rest an error path.

## Examples to copy

### Work out a total

```python filename="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.

```python filename="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

```python filename="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?

- 👉 **[Parse JSON →](/build/action-steps/parse-json)**: turn printed JSON into fields that later Steps can select.
- **[Helper Functions](/build/action-steps/helper-functions)**: use a ready-made conversion instead of maintaining code.
- **[AI Data Transform](/build/ai-features/ai-transform)**: reshape unstructured content without code.
