自定义触发器

内置触发器涵盖了大多数商家关注的变更。自定义触发器则涵盖其余情况:您只需选择一个Shopify事件,编写几行JavaScript代码来判断该事件是否重要以及需要发送哪些数据,它就会成为您可以在Shopify Flow中使用的触发器。

它解决了内置触发器无法解决的三个问题:

  • **仅在满足您设定的条件时才触发。**例如,“只有当订单金额超过500欧元时,才为其添加vip标签”只需一行代码,而不需要创建一个针对每笔订单运行并进行筛选的工作流。
  • 在状态转换时触发,而非在某个状态下。你的代码会看到变化前后的值,因此“状态从草稿变为活跃”或“库存跌破5”这种情况是可能的。而基于当前值的条件无法表达这一点。
  • **仅发送您需要的字段。**将事件重新塑造成您的工作流实际使用的值,而不是逐个读取它们。

自定义触发器甚至不需要Shopify事件:它也可以按计划运行,并自行判断发生了哪些变化。

工作原理

  1. 选择触发条件:一次Shopify事件,还是一个计划任务。
  2. 对于某个事件,您可以选择它监听的事件 - - 即应用程序已经接收到的任何Shopify事件 - - 或者从模板开始,系统会自动选择事件并填充代码。
  3. **捕获真实的有效载荷。**在您的商店中进行相应修改后,该应用便会捕获确切的事件正文。
  4. 编写一个转换函数 - - 若要触发,则返回一个对象;若要跳过,则返回null
  5. 将其与捕获的事件进行对比测试,从而准确了解您的工作流会收到什么内容。
  6. 打开它。
自定义触发器编辑器,包含名称、句柄,可在Shopify事件与“计划任务”之间进行选择,以及事件和模板
一个新的自定义触发器:选择触发条件,指定事件,并可选地基于模板创建。

编写转换规则

您的触发器是一个 JavaScript 模块,该模块导出一个名为 transform 的函数。该函数接收四个参数:

  • payload - 原始的Shopify事件主体,完全按其接收时的原样呈现。
  • topic - 触发了哪个事件,例如PRODUCTS_UPDATE
  • shop - 您的 myshopify 域名。
  • ctx - ctx.log(...) 会在编辑器旁边的日志面板中输出内容,ctx.shopify(...) 会执行一个 Admin GraphQL 查询,而 ctx.fetch(...) 会调用互联网上的某个 URL。

你返回的内容决定了接下来会发生什么:

  • 返回一个对象,触发器随即触发,并携带该对象。
  • 返回null,该事件将被跳过。这就是过滤机制的工作原理 - - 无需学习单独的过滤语言。
  • 将该文件留空,它就会在该类型的每个事件发生时触发。
flow-triggers/high-value-vip-order.jsjavascript
/**
 * 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,
  };
}

变更前的数值

Shopify

在**“产品更新**”、“订单更新”和**“客户更新**”中,payload._changes 会列出此次更新所修改的每个受监控字段,并分别提供 oldValuenewValue

  • 产品:titlehandledescriptionstatusvendorproductTypetags
  • 订单:financialStatusfulfillmentStatustagsnotelineItems 以及 customAttributes.<name>
  • 客户:tagsnotestateENABLEDDISABLEDINVITEDDECLINED

当更新内容不涉及这些字段(例如库存变动)时,或者应用程序首次读取该记录时,该列表为空。你在编辑器中捕获的示例事件会显示该字段,因此你可以在对其进行写入操作之前,先查看其真实结构。

flow-triggers/product-went-live.jsjavascript
/**
 * Fire only when a product BECOMES active - not on every later edit of an
 * active product, which is what a condition on the current status would do.
 */
export async function transform(payload, topic, shop, ctx) {
  const change = (payload._changes || []).find((c) => c.field === "status");
  if (!change) return null;
  if (change.oldValue !== "draft" || change.newValue !== "active") return null;

  return {
    productId: payload.admin_graphql_api_id,
    title: payload.title,
    oldStatus: change.oldValue,
    newStatus: change.newValue,
  };
}

更具体的事件本身就包含新旧值,因此那里无需使用_changes:价格变更包含oldPricenewPricepercentChange;库存变更包含_oldAvailable_newAvailable_delta;元字段变更包含previousValue,以及与其对应的metafield.value

从模板开始

在编辑器中打开“Fires on”后,选择**“从模板开始**”即可选定事件并生成可运行的代码。编辑顶部的常量,进行测试,然后保存。每个模板都会读取更改前后的值:

  • 某产品、订单或客户字段的值发生了变化 - - 状态从“草稿”变为“有效”,订单变为“已付款”,账户变为“已启用”。
  • 价格下降了超过N个百分点 - - 这才是真正的降价,并非每次价格调整都是如此。
  • 库存跌破阈值 - - 触发一次,即在库存刚好跌破该阈值时触发,而非在库存已处于低位时每次销售都触发。
  • 产品元字段数值超过阈值 - - 评分低于3,毛利率高于40。
  • “高价值客户已付款的订单” - - 读取客户在您店铺的累计消费金额,仅当金额超过您设定的阈值时触发。
编辑器中的“Transform”卡片,其中包含一个模板代码,该模板的内容为 payload._changes
模板会自动生成可运行的代码。编辑顶部的常量,进行测试,然后保存。

使用 ctx.shopify 获取额外数据

Webhook 正文仅包含 Shopify 发送的字段。如果您需要其他信息(例如客户的订单数量、变体的库存、元字段等),请在转换中直接查询 Admin API:

Enrich the event before decidingjavascript
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,
  };
}

它会返回查询的data,如果查询出现错误,则会抛出异常,这样错误就会在测试中显现出来,而不是悄无声息地返回空结果。

三个值得了解的限制:

  • **每次执行最多处理 10 个调用。**数据增强只需处理少量数据;超过这个数量通常需要使用循环。只要可能,请尽量通过单个查询获取所需数据。
  • **它只读取数据,不写入数据。**它使用您授予的权限,且该应用仅会请求读取权限。除非在“权限”页面上授予了“订单数据访问”权限,否则查询订单数据将失败。若要在您的商店中进行更改,请在触发器后面的“流程”操作中进行。
  • **您店铺的凭据绝不会进入您的代码。**该查询是由应用代表您执行的,因此沙盒内不存在会泄露的访问令牌。

ctx.fetch(url, options) 其工作原理类似于浏览器中的 fetch,适用于公共互联网上的任何内容:供应商的库存数据流、汇率,或是您自己的 API。内部和私有网络地址将被拒绝。如果您的触发代码已保存到 GitHub,请勿在其中包含 API 密钥。

按计划运行的触发器

Shopify Flow 当发生某些情况时,它会做出反应。如果什么都没发生,它就不会做出反应,而且它也无法察觉商店之外的任何情况。为此,请在**“触发条件**”下选择**“按计划**执行”。此类触发器背后没有Shopify事件:您的代码会按一定间隔运行(从每30秒一次到每天一次不等),并自行判断什么情况算作变化。

这与transform函数相同,只是payload不同,且返回值也不同:

  • payload.state - 无论您的代码在上一次运行时返回了什么state值。首次运行时返回的是null
  • payload.now 以及 payload.lastRunAt - 时间戳。
  • 返回 { state, events }:待保存的新状态(最多 32 KB)以及待触发的事件列表(每次运行最多 100 个)。每个事件都会触发 Custom Trigger 一次;如果设置了事件的 resourceId,该值将成为 Flow 中的记录 ID。若无需报告的内容,则返回 null

**确保首次运行时仅进行记录,切勿触发。**在首次运行时,你的代码没有可供比较的对象,因此一切看起来都是新的。将观察到的内容存储起来,且不返回任何事件;这样,即使启用触发器,也不会导致工作流被大量事件淹没。所有模板都是这样处理的。

“按计划”模式下的自定义触发器编辑器,其中包含间隔时间及其在30天内的运行次数
按计划执行:选择时间间隔 - - 编辑器会显示总共会生成多少次运行 - - 并基于预设模板开始。
flow-triggers/supplier-stock-changed.jsjavascript
// Fires when a value at a JSON URL changes, with the value before and after.
const URL = "https://api.example.com/stock/1042"

export async function transform(payload, topic, shop, ctx) {
  const res = await ctx.fetch(URL, { headers: { accept: "application/json" } })
  if (!res.ok) throw new Error("The URL answered with HTTP " + res.status)
  const value = String((await res.json()).stock)

  // The first run only remembers the value.
  const previous = payload.state ? payload.state.value : undefined
  if (previous === undefined || previous === value) return { state: { value } }

  return {
    state: { value },
    events: [{ oldValue: previous, newValue: value }],
  }
}

“计划”卡片中**“基于模板开始**”下的计划触发器模板:

  • 产品、订单或客户已 N 天未更新 - - 过期的商品评论、滞留订单、客户回流。每个记录在每个“静默期”内触发一次,且在启用该功能时已逾期的记录不会一次性全部触发。
  • Shopify 之外的某个值发生了变化 - - 供应商库存数据、汇率、价格表,其中数值会显示差额和百分比。
  • RSS 或 Atom 源中的新项目 - - 供应商新闻、商品列表源、状态页面。
  • N小时内无订单 - - 这是您店铺的“心跳”。当订单停止时触发一次,订单恢复时再触发一次。

“测试”会将您的代码运行一次,在此过程中既不会触发任何操作,也不会保存状态,因此您可以随心所欲地运行它。

在 Shopify Flow 中使用它

每个自定义触发器在 Flow 中都会以相同的名称显示:自定义触发器。将其添加到工作流中,然后添加一个条件:触发器标识符等于您的触发器标识符。

该标识会显示在触发器的页面上,并且永远不会改变,即使你重命名了该触发器 - - 因此你的工作流仍能正常运行。

转换返回的每个键都会成为触发器上的一个字段,如果你更愿意自己解析该对象,整个对象也可以以 JSON 格式获取。

将代码保存在 GitHub 上

您可以连接一个 GitHub 仓库,让触发器代码置于版本控制之下。这样,更改内容将作为拉取请求供人审查,您可以查看是谁做了哪些修改,并且可以回滚不再起作用的转换。

请在“开发者”页面下的“连接”部分进行连接。您可以选择应用可以访问哪些仓库,并且可以随时在 GitHub 上撤销该访问权限。

一旦选定了一个存储库,所有现有的触发器会立即写入其中,并且该存储库在两个方向上都能保持同步:

你的工作内容 会发生什么
在应用中创建、编辑或复制触发器 该文件已提交到您的代码库
在应用中删除触发器 该文件已从您的代码库中删除
将更改推送到关联的分支 应用中更新了触发器的代码

每个触发器都是一个以该触发器句柄命名的文件,其中包含的正是你在编辑器中看到的那个模块 - - 没有任何额外的包装。这意味着你可以像处理其他任何 JavaScript 文件一样,在自己的编辑器中打开它、运行它并对其进行代码检查。

恢复到较早的版本

无需了解 Git 即可撤销更改。一旦连接了代码库,编辑器会显示一个**“版本**”下拉菜单,其中列出了该文件的所有历史版本,并附有日期和作者信息。选择其中一个版本,它就会以未保存的更改形式加载到编辑器中,这样你就可以先阅读内容 - - 保存操作才会将其恢复。

在本地编辑时进行测试

如果您正在自己的编辑器中编辑该文件,则无需先将其保存到应用中,即可直接使用该文件处理捕获的样本事件。请使用开发者页面中具有**“执行**”权限级别的 API 密钥:

Test the file you are editingbash
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}')"

你会得到与应用程序“测试”按钮显示的结果完全一致的内容:是否会触发、输出对象、你的ctx.log语句以及运行时间。

该端点既不会触发任何操作,也不会保存任何内容,且不占用任何计划配额 - - 因此,在文件监视器每次保存时运行它都是安全的。(应用内的**“运行测试**”按钮会触发您的 Flow 工作流,以便您观察其端到端的运行过程;此操作同样免费。)

您还可以使用读取密钥单独获取示例有效载荷,将其保存到本地,并完全在离线状态下运行该文件:

Get the captured sample payloadbash
curl https://shopify.workflow-trigger-extensions.app/api/v1/triggers/custom/high-value-vip-order/test \
  -H "Authorization: Bearer ftk_your_key_here"
自定义触发器会占用我的套餐配额吗?

是的,它会统计所有监听到的事件 - - 而不仅仅是实际触发的那些。如果你的触发器监听“产品更新”事件,而你的商店每月有 60,000 次产品更新,那么即使你的代码只在其中 100 次更新时被触发,这也仍算作 60,000 个事件。

我们会接收、去重并排队处理每个事件,然后在隔离的沙箱中运行您的代码 - - 所有这些操作都在您的代码决定是否触发之前完成。该计数反映了上述工作量。

换句话说:其费用与同一事件的内置触发器相同。您无需为过滤功能支付额外费用,且测试始终免费。定时触发器每次运行仅计费一次。

检测是免费的吗?

是的。无论是应用中的“运行测试”按钮,还是/test API 端点,均不计入配额,尽管“运行测试”确实会触发您的 Flow 工作流,以便您观察其运行过程。只有实际执行才会消耗您的配额,因此您可以随心所欲地对转换进行迭代。

为什么 payload._changes 为空?

要么是更新内容超出了受监控字段的范围 - - 例如产品的库存或变体变更 - - 要么是该应用尚未为该记录建立基线。基线是在应用首次检测到某条记录时存储的,因此,对于应用从未见过的记录,其首次更新不会包含历史值。此后每次更新都会包含历史值。

我的代码能否修改存储中的数据?

不,ctx.shopify 仅使用您授予的读取权限运行,该应用绝不会请求写入权限。这是有意为之:如果某个触发器修改了其监听的记录,它会重新触发自身,而这正是 Shopify 禁用工作流的循环原因。请在紧随该触发器之后的“流程”操作中修改数据。

我以后可以更换把手吗?

不,这是有意为之。你的 Flow 工作流会根据处理程序名称进行过滤,因此更改处理程序名称会导致该工作流在后台停止运行。你可以随意重命名触发器 - - 处理程序名称保持不变。

该用户名同时也是您 GitHub 仓库中的文件名,因此它也不会发生变化。

如果我的代码有错误,会怎么样?

该事件将被跳过,且错误会记录在触发器中,因此您可以查看出了什么问题。转换失败绝不会阻塞其他任何操作 - - 您的其他触发器(无论是自定义的还是内置的)都会照常运行,不受影响。

我的代码在哪里运行?

在一个与应用程序其余部分隔离的沙箱环境中,该环境设有短暂的时间限制,且无法访问您商店的凭据。它仅能看到您捕获的事件有效载荷,以及您通过 ctx.shopifyctx.fetch 获取的数据。

如果我同时在 GitHub 和应用中编辑该文件会怎样?

最后保存的那一方将胜出。在应用中保存会将更改提交到文件,而推送到关联的分支则会覆盖应用中保存的代码。如果你主要在代码库中工作,请将应用的编辑器视为只读模式,以免给自己造成意外。

我仓库中的文件能否创建一个新的触发器?

不。触发器还需要知道它监听的是哪个Shopify事件,而该文件只包含代码 - - 如果凭猜测确定事件,可能会将其与错误的对象关联起来。请先在应用中创建触发器,然后再自由编辑其文件。

它能否在应用程序尚未接收到的事件发生时触发?

不。自定义触发器会监听应用已为您商店订阅的事件,具体取决于您授予的权限。只要授予某项资源的权限,该资源的事件也会对自定义触发器开放。对于其他情况,请按计划执行。

下一步