Custom triggers
The built-in triggers cover the changes most stores care about. A custom trigger covers the rest: you pick a Shopify event, write a few lines of JavaScript to decide whether it matters and what to send, and it becomes a trigger you can use in Shopify Flow.
Two things it solves that a built-in trigger cannot:
- Fire only on your condition. "Only when an order over 500 EUR gets the
viptag" is one line of code, instead of a workflow that runs on every order and then filters. - Send exactly the fields you want. Reshape the event into the values your workflow actually uses, rather than reading them back out one by one.
How it works
- Pick the event it listens to - any Shopify event the app already receives.
- Capture a real payload. Make that change in your store and the app grabs the exact event body.
- Write a transform - return an object to fire, return
nullto skip. - Test it against the captured event, and see precisely what your workflow would receive.
- Switch it on.
Writing the transform
Your trigger is a JavaScript module that exports a function named transform. It receives four arguments:
payload- the raw Shopify event body, exactly as it arrives.topic- which event fired, for examplePRODUCTS_UPDATE.shop- your myshopify domain.ctx-ctx.log(...)prints to the log panel next to the editor, andctx.shopify(...)runs an Admin GraphQL query.
What you return decides what happens:
- Return an object and the trigger fires, carrying that object.
- Return
nulland the event is skipped. This is how filtering works - there is no separate filter language to learn. - Leave the file empty and it fires on every event of that type.
/**
* Only fire for high-value orders carrying the vip tag.
*/
export async function transform(payload, topic, shop, ctx) {
const total = parseFloat(payload.total_price || "0");
const tags = (payload.tags || "").split(",").map(t => t.trim());
if (total < 500) return null;
if (!tags.includes("vip")) return null;
ctx.log("firing for", payload.name, total);
return {
orderId: payload.admin_graphql_api_id,
orderNumber: payload.name,
total,
currency: payload.currency,
customerEmail: payload.email,
};
}Fetching extra data with ctx.shopify
Webhook bodies only carry the fields Shopify sends. When you need something else - a customer's order count, a variant's inventory, a metafield - query the Admin API directly from your transform:
export async function transform(payload, topic, shop, ctx) {
const data = await ctx.shopify(`
query($id: ID!) {
customer(id: $id) { numberOfOrders tags }
}
`, { id: payload.customer.admin_graphql_api_id });
// Only fire for repeat customers
if (data.customer.numberOfOrders < 5) return null;
return {
orderId: payload.admin_graphql_api_id,
orderCount: data.customer.numberOfOrders,
};
}It returns the query's data, and throws if the query has errors so a mistake shows up in your test rather than silently producing nothing. userErrors from a mutation are printed to the log panel, since those come back with a successful response and are easy to miss.
Three limits worth knowing:
- Up to 10 calls per run. Enrichment needs a handful; more than that is usually a loop. Fetch what you need in one query where you can.
- It uses the permissions you have granted. A query for order data fails unless Order Data Access is granted on the Permissions page.
- Your store's credentials never reach your code. The query is run by the app on your behalf, so there is no access token inside the sandbox to leak.
Using it in Shopify Flow
Every custom trigger arrives in Flow as the same trigger: Custom Trigger. Add it to a workflow, then add a condition on Trigger handle equals your trigger's handle.
That handle is shown on the trigger's page and never changes, even if you rename the trigger - so your workflow keeps working.
Each key your transform returns becomes a field on the trigger, and the whole object is also available as JSON if you would rather parse it yourself.
Keep the code in GitHub
You can connect a GitHub repository so your trigger code lives under version control. Changes become reviewable in a pull request, you can see who changed what, and you can roll back a transform that stopped working.
Connect it on the Developer page, under Connections. You choose which repositories the app can see, and you can revoke that access from GitHub at any time.
Once a repository is selected, every existing trigger is written to it immediately, and it stays in step in both directions:
| What you do | What happens |
|---|---|
| Create, edit or duplicate a trigger in the app | The file is committed to your repository |
| Delete a trigger in the app | The file is removed from your repository |
| Push a change to the connected branch | The trigger's code is updated in the app |
Each trigger is one file named after its handle, containing exactly the module you see in the editor - nothing wrapped around it. That means you can open it in your own editor, run it, and lint it like any other JavaScript file.
Going back to an earlier version
You do not need to know git to undo a change. Once a repository is connected, the editor shows a Version dropdown listing every previous version of that file with its date and author. Pick one and it loads into the editor as an unsaved change, so you can read it first - saving is what puts it back.
Test while you edit locally
If you are editing the file in your own editor, you can run it against the captured sample event without saving it to the app first. Use an API key with the execute level from the Developer page:
curl -X POST https://shopify.workflow-trigger-extensions.app/api/v1/triggers/custom/high-value-vip-order/test \
-H "Authorization: Bearer ftk_your_key_here" \
-H "Content-Type: application/json" \
-d "$(jq -Rn --rawfile c flow-triggers/high-value-vip-order.js '{code:$c}')"You get back the same result the app's Test button shows: whether it would fire, the output object, your ctx.log lines and the run time.
Nothing is fired and nothing is saved by this endpoint, and it does not use any plan allowance - so it is safe to run on every save from a file watcher. (The Run test button inside the app does fire your Flow workflow, so you can watch it run end to end; that is also free.)
You can also fetch the sample payload on its own with a read key, save it locally, and run the file entirely offline:
curl https://shopify.workflow-trigger-extensions.app/api/v1/triggers/custom/high-value-vip-order/test \
-H "Authorization: Bearer ftk_your_key_here"Does a custom trigger use my plan allowance?▾
Yes, and it counts every event it inspects - not only the ones it fires on. If your trigger listens to Product Update and your store has 60,000 product updates a month, that is 60,000 events even if your code fires on 100 of them.
We receive, deduplicate and queue each of those events, then run your code in an isolated sandbox - all before your code decides whether to fire. The count reflects that work.
Put another way: it costs the same as the built-in trigger on the same event would have. You are not charged extra for filtering, and testing is always free.
Is testing free?▾
Yes. Both the Run test button in the app and the /test API endpoint are exempt, even though Run test genuinely fires your Flow workflow so you can watch it run. Only live executions use your allowance, so you can iterate on a transform as much as you like.
Can I change the handle later?▾
No, and that is deliberate. Your Flow workflow filters on the handle, so changing it would silently stop that workflow running. You can rename the trigger freely - the handle stays put.
The handle is also the filename in your GitHub repository, so it never moves either.
What happens if my code has a bug?▾
The event is skipped and the error is recorded on the trigger, so you can see what went wrong. A failing transform never blocks anything else - your other triggers, custom or built-in, carry on unaffected.
Where does my code run?▾
In an isolated sandbox, separate from the rest of the app, with a short time limit and no access to your store's credentials. It only ever sees the event payload you captured, plus whatever you fetch with ctx.shopify.
What if I edit the file in GitHub and in the app at the same time?▾
Whichever you save last wins. Saving in the app commits over the file, and pushing to the connected branch overwrites the code held in the app. If you work mostly in your repository, treat the app's editor as read-only to avoid surprising yourself.
Can a file in my repository create a new trigger?▾
No. A trigger also needs to know which Shopify event it listens to, and the file only contains code - guessing the event would wire it to the wrong thing. Create the trigger in the app first, then edit its file freely.
Can it fire on events the app does not already receive?▾
No. A custom trigger listens to the events the app already subscribes to for your store, which depends on the permissions you have granted. Grant the permission for a resource and its events become available to custom triggers too.
Next steps
- Developer API, MCP, GitHub and trigger simulation - manage and test triggers from your own code or an AI assistant.
- Plans and usage - what counts as an event and how the allowance works.
- How triggers work - the built-in triggers and how they fire.

