How to create a function
A function is a piece of JavaScript or TypeScript that Shopify Flow can run. This page walks through creating one from scratch, testing it, and getting it ready to use in a workflow.
Before you start
Nothing is required to create your first function. If you want your code to read or modify store data, open Permissions first and grant the resources you need (Products, Orders, Customers, Discounts). You can always come back and grant more later.
Step 1: Create the function
- Open Functions in the app navigation.
- Click Create function.
You land in the function editor with starter code already filled in. The editor has five tabs: General, Code, Variables, Test, and Settings.
Prefer to start from something that already works? Click Browse templates on the Functions page instead. Templates cover common jobs such as tagging a customer, hiding out-of-stock products, tagging high-value orders, creating a discount code, and forwarding data to a webhook. Choosing Use creates a real function immediately and drops you in this same editor.
Step 2: Name it (General tab)
- Name is required. This is the name you will pick from the dropdown in Shopify Flow, so make it describe what it does, for example "Tag VIP customers".
- Language is JavaScript or TypeScript. TypeScript types are stripped before execution, so both run the same way.
- Description is optional and only for you and your team.
Step 3: Write the code (Code tab)
Your function must export one default async function that takes (input, ctx):
export default async function (input, ctx) {
// input: the JSON sent by the Flow action
// ctx: helpers provided by the sandbox
ctx.log("running with", input)
return { ok: true, received: input }
}
What you get
| Item | What it does |
|---|---|
input |
The JSON passed in from Flow, or from the Variables tab when testing |
ctx.shop |
Your store domain, for example my-store.myshopify.com |
ctx.log(...) |
Writes a line to the run logs. Accepts several arguments like console.log |
ctx.fetch(url, options) |
Outbound HTTP request. Returns { status, ok, headers, text(), json() } |
ctx.shopify.graphql(query, variables) |
Shopify Admin GraphQL. Only available when Needs Shopify data is on |
secrets.NAME |
A value you saved on the Secrets page |
What you return
Whatever you return becomes the output of the Flow action. Return something that can be serialized to JSON, such as a plain object:
return { tagged: true, total: 129.5 }
Returning nothing is fine too. The output will simply be empty.
Things that are not available
There is no require, no process, no filesystem, and no raw fetch. Nothing persists between runs. Every run starts from a clean sandbox, so if you need to remember state, write it to Shopify (a metafield or a tag) or to your own system.
Step 4: Add test data (Variables tab)
The Variables tab holds a JSON object that is passed to your code as input when you press Run test. It is saved with the function as a sample, so you can come back and re-test later.
{
"customerId": "gid://shopify/Customer/123456789",
"tags": ["vip"]
}
If your function has Needs Shopify data turned on, three buttons appear that fill in real ids from your store, so your first test works against real data:
- Find product opens the Shopify resource picker and inserts the chosen
productId. - Latest order inserts the most recent
orderId. - Latest customer inserts the most recent
customerId.
These variables are test data only. When the function runs from Flow, input comes from the Input (JSON) field on the Run Function action instead.
Step 5: Run a test (Test tab)
Click Run test from the Code or Variables tab. The Test tab shows:
- Status: Success, Failed, or Timeout, with the duration in milliseconds.
- Output: your return value, as a browsable tree.
- Logs: everything you sent to
ctx.log(...), with timestamps. - Error: the message and reason if the run failed.
Test runs do not count against your plan quota, so iterate as much as you like. They are recorded in History with the trigger Manual, so you can look back at them later.
If you have unsaved changes, clicking Run test or switching tabs asks you to save or discard first. Tests always run against saved code, so what you test is what Flow will run.
Step 6: Configure limits and access (Settings tab)
| Setting | Default | Notes |
|---|---|---|
| Enabled | On | A disabled function cannot run from Flow. Existing workflows that use it will report an error |
| Needs Shopify data | Off | Turn on to get ctx.shopify.graphql(...). Also grant the matching permissions on the Permissions page |
| Timeout (ms) | 10000 | Maximum 30000. The run is stopped when the timeout is reached |
| Memory (MB) | 128 | Maximum 512. Raise it only if a run fails on memory |
Keep the timeout as low as your function realistically needs. A tight timeout means a stuck external API fails fast instead of holding up your workflow.
Step 7: Save
Use Save in the save bar at the top of the page. Your function is now available in Shopify Flow.
Using store data
To read or write Shopify data:
- Turn on Needs Shopify data on the Settings tab.
- Open Permissions and grant the resources your code touches. Read and write are granted separately per resource.
- Call
ctx.shopify.graphql(query, variables)in your code.
export default async function (input, ctx) {
const { data } = await ctx.shopify.graphql(`
query($id: ID!) {
product(id: $id) { id title totalInventory status }
}
`, { id: input.productId })
ctx.log("product", data.product.title)
return { inventory: data.product.totalInventory }
}
Your store's access token never enters the sandbox. Calls are proxied server side and Shopify enforces exactly the permissions you granted. If a query needs a permission you have not granted, Shopify returns an access denied error in the response.
If you later revoke a permission on the Permissions page, every function that uses it is disabled automatically and shows a banner telling you which permission is missing. Granting it again re-enables them.
Calling an external API
ctx.fetch works like the browser fetch, with guardrails:
export default async function (input, ctx) {
const res = await ctx.fetch("https://api.example.com/orders", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${secrets.PARTNER_API_KEY}`,
},
body: JSON.stringify(input),
})
if (!res.ok) {
throw new Error(`Partner API returned ${res.status}`)
}
return await res.json()
}
Requests to internal or private network addresses are blocked. Response size, redirects, and per-request time are capped, and there is a limit on how many requests a single run can make.
Editing, deleting, and reusing
- View run history: the primary action on a saved function opens History filtered to that function.
- Save as template: turns the current code into a reusable template. Keep it private to your store, or publish it so other stores can use it. Never publish code that contains business logic or credentials you want to keep private.
- Delete: permanently removes the function. Workflows using it will stop working. Run history is kept. You are asked to confirm first.
Common errors
| Message | What it means |
|---|---|
| Function must export a default async function | Your code has no export default. Add export default async function (input, ctx) { ... } |
| Compile error | A syntax error in your code. The message points at the line |
| TIMEOUT | The run exceeded the timeout on the Settings tab. Usually a slow external API or an endless loop |
| No function configured for this action | The Flow action has no function selected. Open the action in Flow and pick one |
| Function "..." is disabled | The function is turned off on the Settings tab, or was auto-disabled because a permission it needs was revoked |
Next steps
- Set up the Run Function action in Shopify Flow to connect this function to a workflow.
- Passing variables into your function to send real Flow data into your code.
- Creating and using secrets to keep API keys out of your code.