---
title: "Run code and functions - Workflow Suite"
description: "Run your own JavaScript in a workflow step: code in the step or a saved function, with Input, secrets, store data, storage and the key-value store."
canonical: "https://docs.workflow-suite.app/run-code"
---

# Run code and functions

The **Run function** step runs your own JavaScript in a secure sandbox. Use it when a calculation, a data format or a decision is easier to write in code than to click together. The code gets the event and the results of all earlier steps, and what it returns is the step's result.

## Two ways to bring the code

In the step, **Code** offers:

- **Write the code in this step** - the JavaScript lives in the workflow. Good for code only this workflow needs.
- **Run a saved function** - pick a **Function** from your **Functions** page. Good for code several workflows share. A saved function has its own editor, instant test, versions and templates, and can also run on its own schedule or be called over the API.

Under code in a step, **Save as function** turns it into a saved function; the step then runs that function.

## What the code looks like

```js
export default async function (input, ctx) {
  const order = input.event.payload
  const heavy = order.line_items.filter((line) => line.grams > 5000)

  ctx.log("heavy lines", heavy.length)

  return { heavy: heavy.length > 0, skus: heavy.map((line) => line.sku) }
}
```

Later steps read the result as `steps.<id>.output.heavy` and `steps.<id>.output.skus`.

## What the code receives

| Name | Holds |
| --- | --- |
| `input.event` | The trigger's event: `topic`, `payload`, and `changes` for change triggers |
| `input.steps` | The results of all earlier steps, by step id |
| `input.loop` | Inside a Repeat for each: the current item and its position |
| `input.<your field>` | The fields of the step's **Input (JSON)** |
| `secrets.NAME` | A secret from the Secrets page |
| `ctx.log(...)` | Writes to the function run's logs |
| `ctx.fetch(url, options)` | Calls other systems over HTTPS. The only way to the network |
| `ctx.shopify.graphql(query, variables)` | Calls the Shopify Admin API with the permissions you granted |
| `ctx.kv` | The shop's key-value store: `get`, `set`, `delete`, `increment`, `list` |
| `ctx.storage` | Larger JSON documents between runs: `get`, `set`, `delete`, `list` |

## Input (JSON): settings per step

**Input (JSON)** is optional. It is a JSON object whose fields the code reads as `input.tag`, `input.total` and so on. That way one saved function serves many workflows, each with its own settings.

```liquid
{ "tag": "vip", "total": {{ event.payload.total_price | json }} }
```

- Add the `json` filter to text values.
- The names `event`, `steps` and `loop` are taken: they always hold the run's own data.
- The input is kept with the function run in the run history, next to the logs. So it cannot carry secrets: the code reads those as `secrets.NAME` itself, and the editor refuses an input that reads a secret.

> [!WARNING]
> **What code cannot do**
> - **No Files, no SFTP.** Code cannot read or write the Files store and cannot reach SFTP or FTP servers. Use the Files, Download file and File transfer steps for that, and hand their text results to the code through `input.steps`. See [Files, SFTP and downloads](https://docs.workflow-suite.app/files-sftp-and-downloads.md).
> - **No Node.js APIs.** There is no `require`, `process`, file system or raw `fetch`. Network access goes through `ctx.fetch`, which refuses addresses inside private networks.
> - **Fixed limits per run:** 5 seconds, 128 MB of memory and up to 20 outbound requests. Work in batches rather than one call per item.
> - **Nothing carries over in variables.** Every run gets a fresh sandbox. Keep what you need in `ctx.kv` or `ctx.storage`.

## Store data from code

```js
export default async function (input, ctx) {
  const data = await ctx.shopify.graphql(
    `query ($id: ID!) { order(id: $id) { id name displayFinancialStatus } }`,
    { id: input.event.payload.admin_graphql_api_id },
  )
  return data
}
```

Your store's access token never enters the sandbox. The call goes through the app with a short-lived token that is valid for this one run and only for the permissions you granted. Changes made this way are noted for [Loop protection](https://docs.workflow-suite.app/loop-protection.md) like those of any other step.

Often a Shopify query step before the code is simpler: the code then finds the answer in `input.steps`. See [Every Shopify mutation and query](https://docs.workflow-suite.app/shopify-mutations-and-queries.md).

## Testing

- In a **preview**, code does not run, because nobody can know what it would change. The step reports that it was previewed. Choose **Run for real** to run it.
- **Test this step** runs the step alone, or with the steps it depends on.
- A saved function has its own test in the function editor, with sample input.

## Logs and failures

- A code step's run is listed in **Run history** under **Function runs**, with its input, output and logs. Code written in a step is named `Workflow step:` plus the step's name.
- Code that throws, or takes longer than 5 seconds, fails the step with the error message.
- Code that ran and failed is not repeated automatically, because it may already have changed something. Only a busy or unreachable sandbox is retried. See [Automatic retries](https://docs.workflow-suite.app/automatic-retries.md).
- Function runs inside a workflow do not count on their own. The workflow run is what counts against your plan.

## Related

- [Key-value store and Storage](https://docs.workflow-suite.app/key-value-store-and-storage.md)
- [Secrets](https://docs.workflow-suite.app/secrets.md)
- [Liquid script step](https://docs.workflow-suite.app/liquid-step.md) - scripting without JavaScript.
