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.
{ "threshold": 100, "notify": true }
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.
{
"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:
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:
{ "orderName": "{{order.name}}" }
Not this:
{ "orderName": {{order.name}} }
This applies to numbers too. Quote them and convert in code:
{ "total": "{{order.totalPriceSet.shopMoney.amount}}" }
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:
{ "tags": ["vip", "{{order.name}}"] }
Or pass a delimited string and split it in code, which is more forgiving:
{ "tags": "vip,priority" }
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:
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:
{
"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.
Logging the input at the start of a function is also useful while you are building:
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:
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 for values that should never be typed into an input field.
- Set up the Run Function action in Shopify Flow for the surrounding action configuration.