---
title: "Passing variables into your function"
description: "How to fill the input argument from the Flow action's Input (JSON) field, insert Flow variables with Liquid, and avoid an empty input."
canonical: "https://docs.workflow-functions.app/passing-variables-into-your-function"
---

# Passing variables into your function

Your function receives one argument called `input`. This page covers how to fill it from Shopify Flow, how to test with the same shape locally, and the mistakes that cause an empty or broken `input`.

## Where `input` comes from

There are two sources, depending on how the function runs:

| How it runs | Where `input` comes from |
| --- | --- |
| From Shopify Flow | The **Input (JSON)** field on the Run Function action |
| From the editor's Run test | The **Variables** tab of the function |

They are independent. The Variables tab is test data only and never affects live Flow runs. Keeping the two in the same shape is what lets you test realistically.

## The Input (JSON) field

The Input (JSON) field on the Run Function action takes a JSON object. Whatever you put there arrives as `input` in your code.

```json
{ "threshold": 100, "notify": true }
```

```js
export default async function (input, ctx) {
  ctx.log(input.threshold)  // 100
  ctx.log(input.notify)     // true

  return { ok: true }
}
```

Leave the field blank and `input` is an empty object `{}`.

## Inserting Flow variables

Static JSON is rarely what you want. To pass the order, product, or customer that triggered the workflow, insert Flow variables using Liquid. Flow shows an insert-variable control on the field, and it fills in the correct path for your trigger.

```json
{
  "orderId": "{{order.id}}",
  "orderName": "{{order.name}}",
  "total": "{{order.totalPriceSet.shopMoney.amount}}"
}
```

Which variables exist depends on your trigger. An *Order created* trigger has `order`, a *Product added* trigger has `product`, and so on. Use Flow's own variable picker rather than typing paths by hand.

Inside your function these arrive as ordinary values:

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

  return { name: data.order.name }
}
```

### Always quote your variables

Liquid substitutes plain text, so an unquoted variable can produce invalid JSON the moment the value contains a space, a quote, or is empty.

Do this:

```json
{ "orderName": "{{order.name}}" }
```

Not this:

```json
{ "orderName": {{order.name}} }
```

This applies to numbers too. Quote them and convert in code:

```json
{ "total": "{{order.totalPriceSet.shopMoney.amount}}" }
```

```js
const total = Number(input.total)
```

The one exception is when you build a value that is already valid JSON on its own, such as a Liquid-generated array. Even then, quoting and parsing in code is easier to get right.

## What happens with invalid JSON

If the Input (JSON) field is not valid JSON, your function still runs, but `input` is an empty object `{}`. There is no error at the Flow level. Your code will simply see nothing.

This is the single most common reason a function "does nothing" in production while working perfectly in tests. If the input recorded in **History** is `{}` but you expected fields, check the action's Input (JSON) field for:

- an unquoted Liquid variable
- a trailing comma
- a single-quoted string instead of double quotes
- a value that was empty at run time, breaking the surrounding JSON

If you pass valid JSON that is not an object, for example `42` or `"hello"`, it is wrapped as `{ "value": 42 }` so `input` is always an object.

## Passing lists

Two options, both fine:

Build a JSON array with Liquid:

```json
{ "tags": ["vip", "{{order.name}}"] }
```

Or pass a delimited string and split it in code, which is more forgiving:

```json
{ "tags": "vip,priority" }
```

```js
const tags = String(input.tags ?? "").split(",").map((t) => t.trim()).filter(Boolean)
```

## Defaults and validation

Flow variables can be empty. Guard against it rather than assuming:

```js
export default async function (input, ctx) {
  const threshold = Number(input.threshold ?? 100)

  if (!input.orderId) {
    throw new Error("orderId is required. Check the Input (JSON) field on the Flow action.")
  }

  // ...
}
```

Throwing an error like this is deliberate. It fails the Flow step with a message that tells you exactly what to fix, and the message is stored in run history.

## Testing with the same input

The **Variables** tab of the function editor holds the JSON used when you press **Run test**. Mirror the shape your Flow action sends:

```json
{
  "orderId": "gid://shopify/Order/123456789",
  "threshold": 100
}
```

When the function has *Needs Shopify data* enabled, three buttons fill in real ids from your store so your first test works against real data:

- **Find product** opens the Shopify resource picker and inserts `productId`.
- **Latest order** inserts the most recent `orderId`.
- **Latest customer** inserts the most recent `customerId`.

The Variables JSON is saved with the function, so it also serves as documentation of what the function expects.

## Checking what actually arrived

Every run stores the exact input it received. Open **History**, select the run, and look at the input panel. That is the ground truth for what Flow sent, and comparing it against what you expected resolves most input problems in seconds. See [Run history and troubleshooting](https://docs.workflow-functions.app/run-history-and-troubleshooting.md).

Logging the input at the start of a function is also useful while you are building:

```js
ctx.log("input", input)
```

Secret values are automatically masked in logs, so this is safe even if a secret ends up in the data.

## Getting data back out to Flow

The Run Function action returns `status`, `output`, and `durationMs`. `output` is your return value as a JSON string, so Flow cannot read individual fields out of it. If a later Flow step needs one specific value, return that value alone rather than an object:

```js
return "vip"   // output is usable directly in Flow
```

If the decision only matters inside your code, do the branching in the function and act there instead of sending it back to Flow.

## Next steps

- [Creating and using secrets](https://docs.workflow-functions.app/creating-and-using-secrets.md) - values that should never be typed into an input field.
- [Set up the Run Function action in Shopify Flow](https://docs.workflow-functions.app/set-up-the-run-function-action-in-shopify-flow.md) - the surrounding action configuration.
- [Run history and troubleshooting](https://docs.workflow-functions.app/run-history-and-troubleshooting.md) - inspect the input a run actually received.
