ConfigurationTransform functions

Transform functions

Copy page

Run your own function on every webhook request: reshape or enrich the payload, drop requests you do not need, or answer the caller directly.

Mapping picks values out of a request. A transform function goes further: it is your own JavaScript, running on every request before Shopify Flow, so you can reshape the payload, look something up, decide whether the request is worth a workflow run at all, or answer the caller yourself.

It lives on the Function tab of a webhook. An empty editor means off.

The shape of a function

The contractjavascript
export default async function transform(payload, ctx) {
  // payload - the parsed request body (JSON, form or XML)
  // return an object -> it becomes the payload for mapping and Flow
  // return null      -> nothing is sent to Flow, and nothing counts
  return { ...payload, source: "warehouse" }
}

What ctx gives you

ctx.request method, contentType, the request headers (secrets masked) and query
ctx.webhook The webhook's id and name
ctx.shop Your myshopify domain
ctx.log(...) Writes a line you can read in the test panel and in History
await ctx.fetch(url, init) Calls a public API
await ctx.shopify(query, variables) Reads store data with Admin GraphQL
ctx.storage Keeps small values between runs (see below)
ctx.respond(body, options) Answers the caller immediately

Reshaping a payload

The most common use: turn a sender's structure into the few values your workflow needs.

Flatten and normalisejavascript
export default async function transform(payload) {
  const order = payload.data.attributes
  return {
    orderNumber: String(order.number),
    email: order.customer.email.toLowerCase(),
    total: Number(order.total_cents) / 100,
  }
}

Dropping requests you do not need

Return null and the request stops there: no Flow run, no history entry, and nothing counted against your plan. The caller gets 200 with { "skipped": true }, so it does not retry.

Only paid orders above 100javascript
export default async function transform(payload, ctx) {
  if (payload.status !== "paid" || payload.total < 100) {
    ctx.log("skipping", payload.id, payload.status, payload.total)
    return null
  }
  return payload
}

Remembering things between runs

ctx.storage is a small key-value store that belongs to your shop. It is the tool for "have I seen this before?": store an id when an event arrives, and skip the next request that carries the same id, even if it comes days later.

Call Does
await ctx.storage.get(key) The stored value, or null
await ctx.storage.set(key, value) Stores any JSON value, overwriting
await ctx.storage.delete(key) Removes it; true if it existed
await ctx.storage.list({ prefix, limit, cursor }) Key names and sizes, page by page
Skip events you have already seenjavascript
export default async function transform(payload, ctx) {
  const key = `seen:${payload.id}`
  if (await ctx.storage.get(key)) return null
  await ctx.storage.set(key, { at: Date.now() })
  return payload
}

Enriching with store data

ctx.shopify runs read-only Admin GraphQL queries for your store, so a workflow can start with data the sender never had. It only sees what you allow: grant the scopes you need under Store data access on the Function tab. Nothing is granted by default, and mutations are refused.

Look up a variant by SKUjavascript
export default async function transform(payload, ctx) {
  const data = await ctx.shopify(
    `query($q: String!) {
      productVariants(first: 1, query: $q) { nodes { id title price } }
    }`,
    { q: `sku:${payload.sku}` },
  )
  const variant = data.productVariants.nodes[0]
  if (!variant) return null
  return { sku: payload.sku, variantId: variant.id, price: variant.price }
}

Answering the caller yourself

ctx.respond() sends a response back right away, which turns the webhook into a small API endpoint: validate something, compute an answer, and reply in one request. Flow still runs with whatever you return.

Reply to the caller and start a workflowjavascript
export default async function transform(payload, ctx) {
  const ok = typeof payload.email === "string" && payload.email.includes("@")
  ctx.respond({ accepted: ok }, { status: ok ? 200 : 422 })
  return ok ? payload : null
}

Testing

The Test panel on the Function tab runs the code in the editor, including unsaved changes, against a sample payload. It shows the return value, the response your function would send, the Flow fields it produces, your log lines and the duration. Nothing fires Flow, nothing is written to History, and nothing counts toward your plan. Storage calls are real, though: a test run reads and writes the same store as live requests.

When a function fails

If your code throws, times out or returns something unusable, the request is rejected with 422 and the failure is recorded in History with the error and your log lines, so you can see what happened. Fix the code and replay that entry: the replay runs the new version.

Limits

  • 5 seconds and 64MB per run, and 50,000 characters of code.
  • No npm packages. ctx.fetch is the only way out, and internal or private addresses are refused.
  • ctx.shopify allows 10 calls per run, queries only, limited to the scopes you granted.
  • Every run that reaches Flow is a normal invocation and counts toward your plan. A skipped request does not.