From c3da245fb8c82b524feaba9f8f7661e747dead4e Mon Sep 17 00:00:00 2001 From: Gasser Aly Date: Mon, 11 May 2026 13:52:01 -0400 Subject: [PATCH] Add shopify-flow skill Adds skills/shopify-flow as a peer to shopify-admin / shopify-use-shopify-cli. The skill drives Shopify Flow tools through the Shopify CLI (`shopify flow tool call`) via a thin scripts/call_tool.mjs wrapper, keeping backend routing (Flow vs SK) hidden from agents. Op catalog under ops/, reference docs under skills/. Also surfaces Flow tooling in the four plugin/marketplace manifests (.claude-plugin, .codex-plugin, .cursor-plugin, plugin.json), package.json, and README so the new skill is discoverable across all install surfaces. Adds flow/workflows/automation keywords and a Flow example prompt for Codex. Drops the --endpoint flag from skills/shopify-flow/scripts/call_tool.mjs. The wrapper is the agent-facing surface; letting an LLM redirect tool calls to arbitrary URLs is a prompt-injection / token-exfil risk. The override remains available at the CLI layer (shopify flow tool call --endpoint) and via SHOPIFY_FLOW_TOOL_CALL_ENDPOINT for human dev work. --- .claude-plugin/marketplace.json | 9 +- .claude-plugin/plugin.json | 6 +- .codex-plugin/plugin.json | 11 +- .cursor-plugin/marketplace.json | 7 +- .cursor-plugin/plugin.json | 4 +- README.md | 3 +- package.json | 7 +- plugin.json | 7 +- skills/shopify-flow/SKILL.md | 95 +++++ .../skills/building-workflows/SKILL.md | 274 ++++++++++++ .../skills/flow-best-practices/SKILL.md | 144 +++++++ .../skills/running-test-events/SKILL.md | 120 ++++++ .../skills/shopify-search-syntax/SKILL.md | 222 ++++++++++ .../skills/special-tasks/SKILL.md | 393 ++++++++++++++++++ .../shopify-flow/skills/using-flow/SKILL.md | 135 ++++++ .../shopify-flow/skills/workflow-iac/SKILL.md | 158 +++++++ .../skills/workflow-runtime/SKILL.md | 361 ++++++++++++++++ 17 files changed, 1936 insertions(+), 20 deletions(-) create mode 100644 skills/shopify-flow/SKILL.md create mode 100644 skills/shopify-flow/skills/building-workflows/SKILL.md create mode 100644 skills/shopify-flow/skills/flow-best-practices/SKILL.md create mode 100644 skills/shopify-flow/skills/running-test-events/SKILL.md create mode 100644 skills/shopify-flow/skills/shopify-search-syntax/SKILL.md create mode 100644 skills/shopify-flow/skills/special-tasks/SKILL.md create mode 100644 skills/shopify-flow/skills/using-flow/SKILL.md create mode 100644 skills/shopify-flow/skills/workflow-iac/SKILL.md create mode 100644 skills/shopify-flow/skills/workflow-runtime/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bd37149..2c8b6f0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -4,14 +4,14 @@ "name": "Shopify" }, "metadata": { - "description": "Shopify developer tools for Claude Code" + "description": "Shopify developer tools for Claude Code, including Shopify Flow workflow tooling" }, "plugins": [ { "name": "shopify-plugin", "source": "./", - "description": "Search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", - "version": "1.2.1", + "description": "Search Shopify docs, generate and validate GraphQL, Liquid, UI extension code, and Shopify Flow workflows", + "version": "1.5.0", "author": { "name": "Shopify" }, "license": "MIT", "homepage": "https://github.com/Shopify/Shopify-AI-Toolkit", @@ -21,6 +21,9 @@ "graphql", "liquid", "storefront", + "flow", + "workflows", + "automation", "admin-api" ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1f9da58..68d0c47 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "shopify-plugin", - "description": "Shopify developer tools for Claude Code — search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", - "version": "1.2.1", + "description": "Shopify developer tools for Claude Code — search Shopify docs, generate and validate GraphQL, Liquid, UI extension code, and Shopify Flow workflows", + "version": "1.5.0", "author": { "name": "Shopify" }, "license": "MIT", - "keywords": ["shopify", "mcp", "graphql", "liquid", "storefront", "admin-api"] + "keywords": ["shopify", "mcp", "graphql", "liquid", "storefront", "flow", "workflows", "automation", "admin-api"] } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c744e54..70d5240 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,17 +1,17 @@ { "name": "shopify-plugin", "version": "1.2.1", - "description": "Shopify developer tools for OpenAI Codex — search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", + "description": "Shopify developer tools for OpenAI Codex — search Shopify docs, generate and validate GraphQL, Liquid, UI extension code, and Shopify Flow workflows", "author": { "name": "Shopify" }, "homepage": "https://shopify.dev/docs/apps/build/devmcp", "repository": "https://github.com/Shopify/shopify-plugins", "license": "MIT", - "keywords": ["shopify", "graphql", "liquid", "storefront", "admin-api"], + "keywords": ["shopify", "graphql", "liquid", "storefront", "flow", "workflows", "automation", "admin-api"], "skills": "./skills/", "interface": { "displayName": "Shopify", - "shortDescription": "Search Shopify docs, validate GraphQL & Liquid, build Shopify apps faster.", - "longDescription": "Search live Shopify documentation, generate and validate Admin API GraphQL, Storefront API queries, Liquid theme code, and UI extension components — all without leaving your workflow.", + "shortDescription": "Search Shopify docs, validate GraphQL & Liquid, and build Shopify apps and Flow workflows.", + "longDescription": "Search live Shopify documentation, generate and validate Admin API GraphQL, Storefront API queries, Liquid theme code, UI extension components, and Shopify Flow workflows — all without leaving your workflow.", "developerName": "Shopify", "category": "Productivity", "capabilities": ["Read"], @@ -23,7 +23,8 @@ "How do I create a product using the Shopify Admin API?", "Generate a Liquid section with a product image gallery", "Validate my GraphQL query against the Storefront API schema", - "Create a cart validation Function requiring minimum 5 items" + "Create a cart validation Function requiring minimum 5 items", + "Find Shopify Flow templates for fraud prevention" ] } } diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index e22e3c3..f06abd7 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -4,13 +4,13 @@ "name": "Shopify" }, "metadata": { - "description": "Shopify AI toolkit plugins for Cursor" + "description": "Shopify AI toolkit plugins for Cursor, including Shopify Flow workflow tooling" }, "plugins": [ { "name": "shopify-plugin", "source": "./", - "description": "Search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", + "description": "Search Shopify docs, generate and validate GraphQL, Liquid, UI extension code, and Shopify Flow workflows", "version": "1.2.1", "author": { "name": "Shopify" }, "license": "MIT", @@ -21,6 +21,9 @@ "graphql", "liquid", "storefront", + "flow", + "workflows", + "automation", "admin-api" ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 02bf2c0..0c5faac 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,11 +1,11 @@ { "name": "shopify-plugin", - "description": "Shopify developer tools for Cursor — search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", + "description": "Shopify developer tools for Cursor — search Shopify docs, generate and validate GraphQL, Liquid, UI extension code, and Shopify Flow workflows", "version": "1.2.1", "author": { "name": "Shopify" }, "logo": "../assets/shopify_glyph.svg", "homepage": "https://shopify.dev/docs/apps/build/devmcp", "repository": "https://github.com/Shopify/Shopify-AI-Toolkit", "license": "MIT", - "keywords": ["shopify", "mcp", "graphql", "liquid", "storefront", "admin-api"] + "keywords": ["shopify", "mcp", "graphql", "liquid", "storefront", "flow", "workflows", "automation", "admin-api"] } diff --git a/README.md b/README.md index da40cde..d224150 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Connect your AI tools to the Shopify platform. -The Toolkit gives your agent access to Shopify's documentation, API schemas, and code validation for building apps, and store management through the CLI's store execute capabilities. For more info, [see the docs](https://shopify.dev/docs/apps/build/ai-toolkit). +The Toolkit gives your agent access to Shopify's documentation, API schemas, code validation for building apps, store management through CLI capabilities, and Shopify Flow workflow tooling. For more info, [see the docs](https://shopify.dev/docs/apps/build/ai-toolkit). ## Install @@ -35,6 +35,7 @@ The Toolkit gives your agent access to Shopify's documentation, API schemas, and - **Docs and API schemas**: Search Shopify's documentation and API schemas without leaving your editor - **Code validation**: Validate GraphQL queries, Liquid templates, and UI extensions against Shopify's schemas - **Store management**: Manage your Shopify store through the CLI's store execute capabilities +- **Flow workflows**: Search Flow templates and build or update workflows through CLI-backed tooling - **Auto-updates**: The plugin updates automatically as new capabilities are released ## Other install methods diff --git a/package.json b/package.json index d5ade78..5d3b7b6 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "shopify-plugin", - "version": "1.2.1", + "version": "1.5.0", "private": true, - "description": "AI agent plugin manifests for the Shopify Dev MCP server", + "description": "AI agent plugin manifests for the Shopify Dev MCP server and Shopify Flow workflow tools", "author": "Shopify", "license": "MIT", "keywords": [ @@ -11,6 +11,9 @@ "graphql", "liquid", "storefront", + "flow", + "workflows", + "automation", "admin-api", "plugin" ] diff --git a/plugin.json b/plugin.json index 2b8a818..f7e5224 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "shopify-plugin", - "description": "Shopify developer tools for GitHub Copilot — search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code", - "version": "1.2.1", + "description": "Shopify developer tools for GitHub Copilot — search Shopify docs, generate and validate GraphQL, Liquid, and UI extension code, and work with Shopify Flow workflows", + "version": "1.5.0", "author": { "name": "Shopify" }, "license": "MIT", "keywords": [ @@ -10,6 +10,9 @@ "graphql", "liquid", "storefront", + "flow", + "workflows", + "automation", "admin-api", "plugin" ] diff --git a/skills/shopify-flow/SKILL.md b/skills/shopify-flow/SKILL.md new file mode 100644 index 0000000..81869d7 --- /dev/null +++ b/skills/shopify-flow/SKILL.md @@ -0,0 +1,95 @@ +--- +name: shopify-flow +description: "Build, edit, inspect, or debug Shopify Flow workflows. Use for Flow templates, tasks, workflow JSON, environment paths, conditions, Liquid in Flow, shop resource lookups, ShopifyQL fields, test-event planning, and the workflow IaC lifecycle (validate / push / pull / diff / activate). Uses Shopify CLI for execution; do not call Flow or internal HTTP endpoints directly." +compatibility: Requires Node.js and Shopify CLI +metadata: + author: Shopify + version: "0.7.0" +--- + +You are helping a developer work with Shopify Flow workflows from their IDE. +Flow is Shopify's automation product: workflows are directed graphs of triggers, conditions, actions, waits, and loops that run against a shop's data. + +The CLI exposes Flow as first-class commands (no JSON tool dispatch). Discover them with `shopify flow --help` or `shopify flow workflow --help`. + +## Discovery and inspection + +```bash +# Find templates by business goal +shopify flow template search "fraud prevention" "high risk orders" + +# Save a template into the project as a new workflow folder (the "start a new workflow" path) +# `--as` is the folder slug. The template_id comes from a prior `template search` result. +shopify flow template save 01HQK000000000000000000000 --as fraud-prevention + +# Find tasks (triggers, conditions, actions, etc.) +shopify flow task search "order created" "send email" + +# Get a task's full configuration + return-field schema +shopify flow task describe shopify::admin::order_created@0.1 + +# Find environment field paths under an Admin API root type +shopify flow env search Order "customer email" + +# Show a GraphQL type's structure (fields, args, return types) +shopify flow type show Product + +# Resolve the columns a ShopifyQL query produces +shopify flow shopifyql columns "FROM sales SHOW gross_sales BY product_title SINCE -7d" + +# Search for a Shopify resource (routes through Sidekick → Admin API) +shopify flow resource search PRODUCT shoes --limit 5 +``` + +All commands accept `--store `. If you're inside a Flow IaC project (a directory with `flow.toml`), `--store` falls back to the file's `store` field. `--json` is available for scripting. + +When validation fails, the CLI prints the error with the canonical example. Read the error — do not retry with guessed argument names, paths, scopes, fields, or enum values. + +Never call Flow HTTP APIs directly. Never invent or paste bearer tokens. + +## Workflow IaC lifecycle + +Initialize a project and bootstrap from an existing shop: + +```bash +shopify flow init --store shop.myshopify.com +shopify flow workflow pull --all +``` + +Day-to-day: + +```bash +shopify flow workflow validate # dry-run validation, no DB writes +shopify flow workflow push # writes .flow.lock.json +shopify flow workflow pull --workflow-id --out +shopify flow workflow show # print remote JSON to stdout +shopify flow workflow diff # exit 0 = clean, 1 = drift +shopify flow workflow activate # uses lockfile for id+version +shopify flow workflow deactivate +shopify flow workflow list # remote workflows on the shop +shopify flow workflow status # local vs. remote audit +``` + +Load `skills/workflow-iac/SKILL.md` for the full lifecycle walkthrough. + +## Rules + +- Always have a store. Pass `--store`, or run inside a project with `flow.toml`. Ask if neither is available. +- Search templates (`flow template search`) before designing a new workflow. +- `flow task search` then `flow task describe` before referencing a task ID, version, field ID, port, or return field. +- `flow env search` before writing conditions or Liquid paths. +- `flow workflow show` (or `pull`) before modifying an existing workflow. +- Confirm before any mutating call: `workflow push`, `workflow activate`, `workflow deactivate`, test-event firing. +- Surface tool errors directly. Do not retry with guessed values. + +## Reference Files + +Load these only when the task needs them: + +- `skills/workflow-iac/SKILL.md`: IaC lifecycle for git-tracked workflows. +- `skills/building-workflows/SKILL.md`: workflow JSON shape, create/update flow, task discovery, worked examples. +- `skills/workflow-runtime/SKILL.md`: environment variables, condition DSL, Flow Liquid restrictions. +- `skills/special-tasks/SKILL.md`: scheduled triggers, Send Admin API Request, ForEach, Run Code, metafields, ShopifyQL. +- `skills/shopify-search-syntax/SKILL.md`: search query syntax for Get Data tasks. +- `skills/running-test-events/SKILL.md`: test-event and sample-data discipline. +- `skills/flow-best-practices/SKILL.md`: response discipline and workflow quality checks. diff --git a/skills/shopify-flow/skills/building-workflows/SKILL.md b/skills/shopify-flow/skills/building-workflows/SKILL.md new file mode 100644 index 0000000..0a1a9b4 --- /dev/null +++ b/skills/shopify-flow/skills/building-workflows/SKILL.md @@ -0,0 +1,274 @@ +--- +name: building-workflows +description: "How to construct, validate, and submit a Shopify Flow workflow. Load whenever the user asks to create, edit, fix, copy, or delete a workflow. Covers JSON structure, the create-or-update mutation contract, task discovery, the pre-submission checklist, and worked examples." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +Load this skill when you're about to build or change a workflow. Pairs with [`workflow-runtime`](../workflow-runtime/SKILL.md) (env vars, conditions, Liquid) and [`special-tasks`](../special-tasks/SKILL.md) (scheduled triggers, Admin API requests, ForEach, Run Code, metafields, etc.). + +## 1. Workflow JSON structure + +Every workflow you submit to `workflow-create-or-update` must follow this shape: + +```json +{ + "__metadata": { "version": 0.1 }, + "root": { + "workflow_name": "Tag draft orders with NEW", + "steps": [ + { + "step_id": 1, + "task_id": "shopify::admin::draft_order_created", + "task_version": "0.1", + "task_type": "TRIGGER", + "config_field_values": [] + }, + { + "step_id": 2, + "task_id": "shopify::admin::add_draft_order_tags", + "task_version": "0.1", + "task_type": "ACTION", + "config_field_values": [ + { "config_field_id": "draft_order_id", "value": "{\"value\": \"{{ draftOrder.id }}\", \"default_value\": \"\"}" }, + { "config_field_id": "tags", "value": "[\"NEW\"]" } + ] + } + ], + "links": [ + { "from_step_id": 1, "from_port_id": "output", "to_step_id": 2, "to_port_id": "input" } + ], + "patched_fields": [] + } +} +``` + +Required structure: + +- `__metadata` and `root` at the top level. +- Inside `root`: `workflow_name`, `steps`, `links`, `patched_fields`. +- `workflow_name` uses sentence case ("Cancel high risk orders for customer", not Title Case). +- Each step has `step_id`, `task_id`, `task_version`, `task_type`, `config_field_values`. +- At least one `TRIGGER` step (entry point) and at least one `ACTION` step. + +### Task types (UPPERCASE) + +- `TRIGGER` — starts execution +- `ACTION` — performs operations +- `CONDITION` — branching logic +- `FOREACH` — iterate over a list +- `WAIT` — pause execution + +## 2. Creating vs. updating + +### To CREATE a new workflow + +Build the JSON, then call: + +```bash +scripts/call_tool.mjs --op workflow-create-or-update \ + --store \ + --arguments-file /tmp/wf.json +``` + +where `/tmp/wf.json` contains `{"workflow_json": {...}}`. Leave `workflow_id` and `workflow_version` unset. + +### To UPDATE an existing workflow + +1. Look up the current state: + ```bash + scripts/call_tool.mjs --op workflow-lookup \ + --store \ + --arguments '{"workflow_id":"gid://flow/Workflow/123"}' + ``` +2. Modify the returned JSON (only what the user asked — preserve everything else). +3. Submit with both `workflow_id` AND `workflow_version` set: + ```json + { "workflow_id": "gid://flow/Workflow/123", "workflow_version": "v7", "workflow_json": { ... } } + ``` + +When to leave the IDs empty: +- Brand-new workflow. +- User explicitly asks to copy/duplicate. + +When to keep the existing IDs: +- Fixing validation errors and retrying. +- User reports issues with their current workflow. +- Any modification to existing work. + +### Preserving user modifications + +When updating, the user may have made manual changes since the workflow was last automated. Read the full current state via `workflow-lookup` (or from the IDE's `.workflow` context if you've been given it), identify what's different from your mental model, and only change what the user asked. **Make surgical changes, preserve user work.** Don't rebuild from scratch. + +## 3. Task discovery (the two-step workflow) + +Before referencing any task, look it up. Don't memorize task IDs — verify them. + +**Step 1 — search by natural language:** + +```bash +scripts/call_tool.mjs --op task-search \ + --store \ + --arguments '{"search_queries":["order created","add order tags","send email"]}' +``` + +Returns `{id, label, description, publisher, installed, version}` per task. Selection priority: + +1. Prefer `installed: true` over `installed: false`. +2. Publisher priority: `shopify` > `flow` > `flow-connectors` > `shopify-app` > `non-shopify-app`. +3. Best semantic match if priority is tied. + +**Step 2 — get configuration details:** + +```bash +scripts/call_tool.mjs --op task-configuration \ + --store \ + --arguments '{ + "tasks":[ + {"id":"shopify::admin::order_created","version":"0.1"}, + {"id":"shopify::admin::add_order_tags","version":"0.1"} + ] +}' +``` + +Returns `config_fields` JSON (with validation rules, accepted values), `return_fields` JSON (the output shape), `task_type`, `input_port_id`, and `output_port_ids`. + +**`task-configuration` is the source of truth for `config_field_id` values, query filters, and return fields. Never guess.** + +## 4. Config field values + +```json +"config_field_values": [ + { "config_field_id": "field_name", "value": "field_value" } +] +``` + +`config_field_id` values come from the `config_fields` JSON returned by `task-configuration`. The `value` is always a string, but the *content* of the string varies by field type — sometimes plain text, sometimes JSON, sometimes a Liquid-templated wrapper like `{"value": "{{ ... }}", "default_value": ""}`. Read the `config_fields` schema for the field to know which. + +## 5. Errors and retry + +The `workflow-create-or-update` op returns errors in named buckets: + +- **`configFieldErrors`** — invalid config values, bad variable paths, typos, wrong formats. Fix with [`environment-paths-search`](../../ops/environment-paths-search.json) for path issues, or re-read `task-configuration` for field shape. +- **`conditionErrors`** — condition DSL syntax errors. See [`workflow-runtime`](../workflow-runtime/SKILL.md) §Conditions. +- **`foreachBodyErrors`** — wrong variable scope, ForEach item used outside the loop body. Check that `Foreachitem` is only referenced inside steps connected to the `loop_body` port. +- **`taskErrors`** — task configuration issues. Re-read `task-configuration`. + +When you get these errors, **fix them and retry with the SAME `workflow_id` + `workflow_version`** — do not create a new workflow. + +**Never hand off a workflow with fixable validation errors.** The merchant cannot triage them. + +### Informational errors (do NOT retry) + +- Marketing automation email tasks — require manual setup in the merchant's tools. +- Transactional email connectors — require third-party apps to be installed. +- Iris alert tasks — hardcoded configuration; nothing to fix. + +Tell the user what they need to do and stop. + +## 6. Pre-submission checklist + +Before calling `workflow-create-or-update`, verify: + +- [ ] JSON has `__metadata` + `root` with `workflow_name`, `steps`, `links`, `patched_fields`. +- [ ] Task types are UPPERCASE: `TRIGGER`, `ACTION`, `CONDITION`, `FOREACH`, `WAIT`. +- [ ] At least one `TRIGGER` and at least one `ACTION`. +- [ ] Variable names are lowercase + direct access (no `.edges`, `.nodes`, `[0]`). +- [ ] Conditions use DSL syntax only — no Liquid `{{ }}` inside conditions. +- [ ] Liquid outputs only scalar values — no `| json` filter, no outputting full objects/arrays. +- [ ] All variable paths verified via `environment-paths-search` (no guessed paths). +- [ ] All task IDs and `config_field_id` values verified via `task-configuration` (no guessed names). +- [ ] All resource references (products, customers, collections...) resolved via `search-shop-resource` before being inlined. + +If you cannot tick a box, stop and resolve before submitting. + +## 7. Worked examples + +Links shorthand: `step_id[port]→step_id`. The `to_port_id` is always `"input"`. + +- `[output]` — port for `TRIGGER` and `ACTION` steps. +- `[true]` — `CONDITION` true branch (NOT `"output"`). +- `[false]` — `CONDITION` false branch. +- `[loop_body]` — `FOREACH` loop body. +- `[after]` — runs once after `FOREACH` completes. + +### 7.1 Simple condition branching: tag high-value orders + +Steps: + +1. `TRIGGER` `shopify::admin::order_created` +2. `CONDITION` `shopify::flow::condition` — `(order.totalPrice > 500)` +3. `ACTION` `shopify::admin::add_order_tags` — tags: `["High-Value"]` + +Links: `1[output]→2`, `2[true]→3`. + +### 7.2 ForEach with nested condition: tag products in presale orders + +Steps: + +1. `TRIGGER` `shopify::admin::order_created` +2. `CONDITION` `(order.lineItems any? |lineItems_item| (lineItems_item.product.tags any? |tags_item| (tags_item == 'presale')))` +3. `FOREACH` `shopify::flow::foreach` on `order.lineItems` +4. `CONDITION` (in loop) `(lineItemsForeachitem.product.tags any? |tags_item| (tags_item == 'presale'))` +5. `ACTION` (in loop) `shopify::admin::add_product_tags` — tags: `["Ordered-Presale"]`, `product_id: lineItemsForeachitem.product.id` +6. `ACTION` (after loop) `shopify::flow::send_email` — staff notification + +Links: `1[output]→2`, `2[true]→3`, `3[loop_body]→4`, `4[true]→5`, `3[after]→6`. + +Key: the `loop_body` port runs steps inside the loop, `after` runs once when the loop finishes. Inside the loop, `lineItemsForeachitem` exposes each item. + +### 7.3 Branching on both true and false: fraud prevention + +Steps: + +1. `TRIGGER` `shopify::admin::order_risk_analyzed` +2. `CONDITION` `(orderRiskLevel != 'High')` +3. `CONDITION` `(order.capturable == true)` +4. `ACTION` `shopify::admin::capture_payment` +5. `ACTION` `shopify::admin::add_order_tags` — tags: `["Payment-Hold"]` +6. `ACTION` `shopify::flow::send_email` — fraud alert + +Links: `1[output]→2`, `2[true]→3`, `2[false]→6`, `3[true]→4`, `3[false]→5`. + +Both `[true]` and `[false]` ports can connect to different actions — that's how you express if/else. + +### 7.4 Scheduled trigger with Get Data: daily unfulfilled orders report + +Steps: + +1. `TRIGGER` `shopify::flow::scheduled_time` — daily at 9AM in shop's timezone (RRULE: `DTSTART;TZID=America/New_York:20250620T090000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO`). +2. `ACTION` `shopify::flow::fetch::orders` — query: `fulfillment_status:unfulfilled AND NOT status:cancelled`, sort by `CREATED_AT` desc, max 100. +3. `ACTION` `shopify::flow::send_email` — Liquid template loops over `getOrderData`. + +Links: `1[output]→2`, `2[output]→3`. + +Key: scheduled triggers use RRULE format with IANA timezone from shop context. `getOrderData` is a list — see [`workflow-runtime`](../workflow-runtime/SKILL.md) §Step output variables for how to count/iterate it. DTSTART must be a future date. + +### 7.5 Admin API chaining: Order Edit (begin → modify per item → commit) + +Steps: + +1. `TRIGGER` `shopify::admin::order_created` +2. `ACTION` `shopify::admin::admin_api_operation` — `orderEditBegin` +3. `ACTION` `shopify::admin::admin_api_operation` — `orderEditAddShippingLine`, using `{{ sendAdminApiRequest.calculatedOrder.id }}` from step 2 +4. `FOREACH` `shopify::flow::foreach` on `order.lineItems` +5. `ACTION` (in loop) `shopify::admin::admin_api_operation` — `orderEditAddLineItemDiscount`, using `{{ sendAdminApiRequest.calculatedOrder.id }}` and `{{ lineItemsForeachitem.id }}` +6. `ACTION` (after loop) `shopify::admin::admin_api_operation` — `orderEditCommit` + +Links: `1[output]→2`, `2[output]→3`, `3[output]→4`, `4[loop_body]→5`, `4[after]→6`. + +Key: each Admin API request's response becomes a chained variable (`sendAdminApiRequest`, `sendAdminApiRequest1`, ...). See [`special-tasks`](../special-tasks/SKILL.md) §Send Admin API Request for the `name`/`blob` shape. + +### 7.6 Run Code for filtering + aggregation: alert on high-value fragile items + +Steps: + +1. `TRIGGER` `shopify::admin::order_created` +2. `ACTION` `shopify::flow::run_code` — sums prices of fragile-tagged line items +3. `CONDITION` `(runCode.fragileTotal > 200)` +4. `ACTION` `shopify::flow::send_email` — alert + +Links: `1[output]→2`, `2[output]→3`, `3[true]→4`. + +Run Code is appropriate here because Sum and the condition DSL alone can't filter items by tag and then aggregate. See [`special-tasks`](../special-tasks/SKILL.md) §Run Code Action for the config field structure and the input/script/output_schema contract. diff --git a/skills/shopify-flow/skills/flow-best-practices/SKILL.md b/skills/shopify-flow/skills/flow-best-practices/SKILL.md new file mode 100644 index 0000000..7683f6c --- /dev/null +++ b/skills/shopify-flow/skills/flow-best-practices/SKILL.md @@ -0,0 +1,144 @@ +--- +name: flow-best-practices +description: "Best practices and response guidelines for Flow workflow tasks: workflow construction, condition building, Liquid code, error handling, finding shop data, when to use placeholders, and the response format (short, terse, no headers, call out placeholders). Always load alongside using-flow — these rules govern every Flow interaction." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +This skill is **always loaded alongside [`using-flow`](../using-flow/SKILL.md)**. The transport rule and the never-fabricate rule live in `using-flow`; the rules below govern the *shape* of your work. + +## Workflow construction + +1. **Search templates first.** Run `template-search` before building any workflow from scratch. Templates are expert-built and often solve the request directly or reveal patterns to copy. +2. **Use the exact JSON structure** from [`building-workflows`](../building-workflows/SKILL.md) §1. Don't invent new top-level keys, don't omit `__metadata` or `patched_fields`. +3. **Check trigger data first.** Most data is already hydrated by the trigger — do NOT add Get Data steps for the same object that triggered the workflow. +4. **Never guess field paths.** Always verify with `environment-paths-search` before referencing a path in a condition, Liquid template, or task config. +5. **Minimize Get Data actions.** Each one is a real API call; only fetch what's not in the trigger. +6. **Use descriptive step names.** They become variable names — `Get unfulfilled orders` reads better than `Get orders 1` and `getUnfulfilledOrders` is more useful than `getOrders1`. +7. **Plan step dependencies.** Before adding a step, check what variables are available at that point in the graph. ForEach items, in particular, are scoped to the loop body. + +## Condition building + +1. **Search fields first** with `environment-paths-search` before writing any condition. +2. **Match operators to data types.** String operators on string fields, `nil?`/`not_nil?` on non-strings, array operators on lists. +3. **Use parentheses** for complex logic. +4. **Test edge cases:** empty lists, null fields, missing optional fields. +5. **Never use Liquid in conditions.** The condition DSL is its own language — see [`workflow-runtime`](../workflow-runtime/SKILL.md) §Condition DSL. + +## Liquid code + +1. **Always loop through lists** — never use array indexes (`[0]`, `[1]`, ...). +2. **Check for empty/nil before nested property access.** `{% if order.customer %}{{ order.customer.email }}{% endif %}`. +3. **Use comments only when the WHY is non-obvious.** Don't narrate what the Liquid does — the reader can read it. +4. **Test realistically.** Use a test event with realistic data shape (see [`running-test-events`](../running-test-events/SKILL.md)). +5. **Follow Flow's restrictions.** No `size` filter on objects, no `.first`/`.last`/`.empty?`, no `| json` on objects, no array indexing. See [`workflow-runtime`](../workflow-runtime/SKILL.md) §Liquid. + +## Error handling + +1. **Provide fallbacks for optional data.** `{{ order.customer.email | default: "no email on file" }}`. +2. **Handle empty lists.** Always check `{% assign count = list.id | size %}{% if count > 0 %}` before iterating. +3. **Use existence checks before nested access.** `{% if order.customer and order.customer.firstName %}`. +4. **Inform the user about manual steps.** Some tasks (marketing email, transactional email connectors, alert tasks) require setup outside Flow. Tell the user clearly. +5. **Use placeholder values when you have to.** See "Finding shop data and using placeholders" below. + +## Workflow building process + +When you receive a request: + +1. **Search templates first** — `template-search`. Templates may solve the problem directly. +2. **Think through the data flow** before responding. Trigger → conditions → actions. What variables exist at each point? Which paths need verification? +3. **Use the two-step task discovery** (`task-search` → `task-configuration`). +4. **Verify all variable paths** with `environment-paths-search` before using them. Never guess `order.customer.totalSpent` — search for `totalSpent` on `Customer` first. +5. **Use the right adjacent skill when stuck.** If templates and path searches don't reveal the data or operation you need, use the relevant Shopify AI Toolkit/Admin GraphQL skill if it is installed, or ask for the missing capability to be added to the Flow tool catalog. Do not call Flow directly as a workaround. +6. **Provide complete configurations** in the proper JSON format. No partials, no "fill in the rest yourself." +7. **Use best judgment.** Don't ask the user to choose technical details (port names, task versions, internal IDs) — pick a reasonable default and tell them what you chose. +8. **Use the exact structure** from [`building-workflows`](../building-workflows/SKILL.md) §1. + +## Finding shop data and using placeholders + +Priority order for shop-specific values: + +1. **Use shop context first.** Check whatever `` context the IDE has provided you (the merchant's `shopTimeZone`, `shopCurrencyCode`, `primaryDomain`, etc.). +2. **Use `search-shop-resource`** when the user mentions a specific resource by name. See list below. +3. **Use an Admin GraphQL-capable skill/tool** when `search-shop-resource` doesn't support the resource type or you need a complex filter. +4. **Use placeholders** only when the data cannot be inferred from context. + +Use shop context for: + +- Timezone in scheduled triggers (`shopTimeZone` or the user's `userTimeZone`). +- Currency awareness when discussing prices. +- Country-specific formatting/logic. +- Personalizing workflow content with `shopName`. + +Use `search-shop-resource` for: + +- **Products / variants by name** ("add product X", "update Blue T-Shirt") → product/variant GID. +- **Collections by name** ("tag products in Summer Sale") → collection GID. +- **Customers by name/email** ("tag customer john@example.com") → customer GID. +- **Locations by name** ("fulfill from Warehouse A") → location GID. +- **Discounts by name** ("apply SUMMER20") → discount GID. +- **Metaobjects by name** ("get FAQ entry X") → metaobject GID. + +If the workflow logic references a specific resource by name, you **MUST** resolve it to a GID via `search-shop-resource` before building. Don't put the name into a config field — Flow will reject it. + +Fall back to Admin GraphQL when: + +- The resource type isn't supported by `search-shop-resource` (inventory items, fulfillment services, shipping zones). +- You need to query by fields `search-shop-resource` doesn't filter on (metafield values, complex nested filters). + +Use placeholders only when the user hasn't provided specific values **and** the data cannot be inferred from shop context or conversation: + +- **Email addresses:** `staff@example.com`, `merchant@example.com`. +- **Phone numbers:** `+1-555-555-5555`. +- **URLs:** `https://api.example.com/webhook`. +- **API keys:** `YOUR_API_KEY`, `SHOPIFY_API_KEY`. +- **GIDs:** `gid://shopify/Product/1234567890`, `gid://shopify/Customer/1234567890` — only as last resort, AFTER trying `search-shop-resource`. + +**Always call out placeholders briefly** in your reply: "Replace `staff@example.com` with your actual email." + +## Response format + +**Keep responses SHORT.** The merchant cares about the workflow, not your prose. + +DO NOT: + +- Use markdown headers (`#`, `##`, `###`) unless the response is genuinely structured (e.g. several distinct workflows). +- Write long explanations of what the workflow does — the JSON is the source of truth. +- Create lengthy "What This Workflow Does" sections. +- Add verbose "Next Steps" lists. + +DO: + +- Briefly confirm the workflow was created. +- Call out any placeholders that need updating (one line each). +- Mention any limitations or manual setup briefly. +- Keep total response to 2–4 sentences when possible. + +Good: + +``` +Created the order notification workflow. Replace `staff@example.com` with your actual staff email. The workflow will trigger on new orders and send the email with order details. +``` + +Bad (too verbose): + +``` +## What This Workflow Does +**Trigger:** Starts automatically whenever an order is created in your shop... +**Action:** Sends a detailed email notification... +## Next Steps: +1. Review the workflow... +``` + +## When to inform the user + +Always tell the user when: + +- Manual configuration is required (email templates, third-party connector setup). +- You used placeholder values that need replacement. +- The workflow has limitations or constraints (rate limits, scope of data, etc.). +- They can manually trigger the workflow for testing. +- Additional setup steps are needed outside Flow. + +Don't bury these in prose. One line per item. The merchant should see them at a glance. diff --git a/skills/shopify-flow/skills/running-test-events/SKILL.md b/skills/shopify-flow/skills/running-test-events/SKILL.md new file mode 100644 index 0000000..53f08ad --- /dev/null +++ b/skills/shopify-flow/skills/running-test-events/SKILL.md @@ -0,0 +1,120 @@ +--- +name: running-test-events +description: "Run test events through a Shopify Flow workflow during development. Use when the user wants to verify a workflow works, debug why a step is firing or not firing, generate a representative trigger payload, or build up a sample-data session for a workflow. The discipline is **never fabricate, verify literally** — every field of an event must come from the real trigger schema, never from memory." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +A test event is a single trigger payload fed through a workflow during development to confirm it behaves as expected. The model for this in Flow is the **sample-data session**: a workflow has zero or one sample-data session, the session has zero or more events, and each event is either captured (from a real run) or custom (synthetic). All the test-event work in this skill goes through that session model. + +This skill assumes you have already loaded `using-flow`. If you have not, load it now — that skill establishes the CLI transport rule and the never-fabricate rule that this skill leans on. + +## The two literal failure modes you are guarding against + +1. **Field-name fabrication.** You write `customer_id` in the event payload when the trigger's schema actually says `customerId`, and the workflow happily evaluates conditions against `null`. Validation passes; behavior is wrong. +2. **Premature "verified".** You fire one event that takes the happy path through every condition and call the workflow done. The condition the merchant actually cares about — the one that gates whether the action fires — was never exercised. + +The rule that prevents both: get the real schema first, then construct the event, then exercise the conditions you care about (not the easy ones). + +## The discipline + +1. **Get the real trigger schema.** Before constructing any payload, fetch the JSON schema for the workflow's trigger through the Flow tool catalog once the test-event tools are exposed. Do not work from memory or from the trigger's name alone. +2. **Use real captured events when available.** If the workflow already has captured events on its session, prefer cloning one of those over building from the schema cold — the captured event has the actual shape and value distribution from the merchant's store. +3. **Mark every value you made up.** When the schema requires a field whose specific value you had to invent (an ID, a timestamp, an email), flag it explicitly in the response to the user. Do not let them assume a synthetic value is realistic. +4. **Fire the event end-to-end and read the run output.** A passing event is not the same as a correct workflow. Always check what each step actually emitted, not what you expected it to emit. +5. **Hit the conditions the merchant cares about.** A test event that takes the workflow's happy path tells you almost nothing about the conditions. Pick payloads that exercise the branches that gate the actions, not the default path. + +## Auth and transport + +Same rule as `using-flow`: every call goes through `scripts/call_tool.mjs`. Never call Flow's HTTP API directly. + +For every call, pass `--store `. If the user has not given you a shop domain, ask — do not guess. + +## Worked sequence: build and verify a custom event + +The synthetic-event tools are not in the V0 op catalog yet. When they are exposed, they should follow the same shape as the rest of the bundle: checked-in JSON specs under `ops/` and calls through `scripts/call_tool.mjs --op `. + +**1. Look up the workflow you are working with.** Get its ID and its current trigger. + +```bash +scripts/call_tool.mjs --op workflow-lookup \ + --store \ + --arguments '{"workflow_id":""}' +``` + +**2. Get the trigger schema for that workflow.** This tells you exactly what fields a custom event payload must contain. + +```bash +scripts/call_tool.mjs --op \ + --store \ + --arguments '{"workflow_id":""}' +``` + +If a test-event schema tool is not available in `ops/`, stop and say the tool catalog does not expose synthetic-event tooling yet. Do not iterate by guessing. + +**3. Read the existing sample-data session.** If there are captured events, prefer one of them as a starting point. + +```bash +scripts/call_tool.mjs --op \ + --store \ + --arguments '{"workflow_id":""}' +``` + +**4. Create a custom event.** Save the arguments to a file when the payload is non-trivial. + +```bash +scripts/call_tool.mjs --op \ + --store \ + --arguments-file /tmp/flow-test-event.json +``` + +Variables (saved to a file because they are non-trivial): + +```json +{ + "workflow_id": "", + "events": [{ + "event_name": "high-value-international-order", + "trigger_params": { "<...real fields from step 2's schema...>" } + }] +} +``` + +Mark every invented value before sending. Do not silently generate timestamps or IDs. + +**5. Fire the event and read the run.** + +```bash +scripts/call_tool.mjs --op \ + --store \ + --arguments '{"workflow_id":"","event_id":""}' +``` + +After firing, walk the resulting workflow-run / step-run records — every step's actual outputs, not just whether the run succeeded. If the tool catalog does not expose a run/read tool yet, stop and say so. + +## What "never fabricate" means here, concretely + +- If the trigger schema says `currency: String!` with an enum-like description, list the valid values from the schema and pick one. Do not write `"USD"` because it is the most common. +- If the schema says a field is one of an `enum`, list the enum members from the IDL and pick one. Do not write `"FULFILLED"` when the real values are lowercase or vice versa. +- If the schema describes nested objects (line items, addresses, metafields), construct each nested object the same way: real schema, real shape, marked guesses. +- If the workflow's conditions reference an optional field, include it in the payload. Flow evaluates `null` and `missing` differently than the merchant likely expects, and that gap is a common silent-failure mode. + +## When the user asks for "a quick test event" + +There is no quick test event for a workflow that branches on a real condition. A payload that takes the happy path through every condition exercises nothing of value. + +Ask: "which condition do you most need this to exercise?" Then construct a payload that fails that condition and one that passes it, and fire both. If the workflow has multiple conditions, expect to fire several events, not one. + +## Failure modes worth flagging back to the user + +- **Run completes, no action fired.** Almost always a condition evaluated against a missing or wrongly-named field. Diff the event payload against the condition's expression — and against the trigger schema — before retrying. +- **Run completes, action fired on the wrong data.** The action's Liquid is reading a different path than the condition tested. Re-fetch the trigger schema, then walk both the condition and the action against it. +- **Workflow ran twice in production but not in test.** Test events do not catch update-vs-transition triggers. If the trigger config is "on every update," say so explicitly to the user — this class of bug only surfaces in real traffic. + +## What this skill never does + +- Construct a test event without the real trigger schema in hand. +- Declare a workflow verified after one happy-path test event. +- Fabricate an enum value, a field name, or a workflow ID. Every such value comes from a real query or is explicitly marked as a guess. +- Run a mutation (creating events, firing events, deleting events) without summarising and confirming with the user first. diff --git a/skills/shopify-flow/skills/shopify-search-syntax/SKILL.md b/skills/shopify-flow/skills/shopify-search-syntax/SKILL.md new file mode 100644 index 0000000..19afb2a --- /dev/null +++ b/skills/shopify-flow/skills/shopify-search-syntax/SKILL.md @@ -0,0 +1,222 @@ +--- +name: shopify-search-syntax +description: "Shopify search query syntax for Get Data tasks (orders/customers/products fetches). Comparators, connectives, modifiers, exists/prefix queries, and the per-resource filter sets. Load whenever you're configuring a query string for shopify::flow::fetch::* or any task that takes a Shopify search query." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +This skill is for the `query` config field on Get Data tasks (`shopify::flow::fetch::orders`, `_::customers`, `_::products`) and any other task whose `config_fields` declares a Shopify-search-style filter. + +## Source of truth + +**The available filters for a specific task are whatever its `task-configuration` response declares.** Don't memorize the list below as canonical — verify per task: + +```bash +scripts/call_tool.mjs --op task-configuration \ + --store \ + --arguments '{ + "tasks":[{"id":"shopify::flow::fetch::orders","version":"0.1"}] +}' +``` + +Read the `config_fields` in the response. Only use filters that appear there. If the filter you want isn't present, leave the query blank or take a different approach. + +## Common Get Data task IDs + +- Orders: `shopify::flow::fetch::orders` +- Customers: `shopify::flow::fetch::customers` +- Products: `shopify::flow::fetch::products` + +## Query syntax + +### Comparators + +| Operator | Meaning | +|---|---| +| `:` | equality (exact match for non-tokenized fields, contains for tokenized fields) | +| `:<` | less than | +| `:>` | greater than | +| `:<=` | less than or equal | +| `:>=` | greater than or equal | + +### Connectives + +- `AND` — both conditions must match (default if omitted). +- `OR` — either condition. + +### Modifiers + +- `-field:value` or `NOT field:value` — exclude documents matching. + +## Examples + +Field search: + +``` +first_name:Bob age:27 +``` + +Range: + +``` +orders_count:>16 orders_count:<=30 +``` + +NOT: + +``` +-tag:wholesale +NOT status:cancelled +``` + +Boolean: + +``` +(status:open OR status:closed) AND financial_status:paid +``` + +Phrase (use quotes for tokenized fields): + +``` +first_name:"Bob Norman" +``` + +Prefix: + +``` +norm* +``` + +Exists / does-not-exist: + +``` +published_at:* +-published_at:* +``` + +## Order query filters + +**Basic:** + +- `name:1001-A` +- `id:1234` or `id:>=1234` +- `status:(open|closed|cancelled)` +- `created_at:2020-10-21T23:39:20Z` + +**Financial:** + +- `financial_status:(paid|pending|authorized|partially_paid|partially_refunded|refunded|voided)` +- `gateway:shopify_payments` +- `discount_code:ABC123` + +**Fulfillment:** + +- `fulfillment_status:(unfulfilled|fulfilled|partial|scheduled|on_hold)` +- `delivery_method:(shipping|pick-up|retail|local)` + +**Customer:** + +- `email:example@shopify.com` +- `customer_id:123` +- `channel:web` or `channel:web,pos` + +**Other:** + +- `risk_level:(high|medium|low|none)` +- `tag:my_tag` / `tag_not:my_tag` +- `sku:ABC123` + +## Product query filters + +- `title:The Minimal Snowboard` +- `status:(ACTIVE|ARCHIVED|DRAFT)` +- `sku:XYZ-12345` +- `inventory_total:>150` +- `out_of_stock_somewhere:true` +- `tag:my_tag` + +## Customer query filters + +**Basic:** + +- `default` — case-insensitive search across multiple fields. Examples: `Bob Norman`, `title:green hoodie`. +- `id:1234` or `id:>=1234` +- `email:gmail.com` or `email:"bo.wang@example.com"` (tokenized — quote for exact). +- `email:*` — has any email. +- `phone:+18005550100` or `phone:*`. + +**Name:** + +- `first_name:Jane` +- `last_name:Reeves` + +**Address:** + +- `country:Canada` or `country:JP` (full name or two-letter). + +**Dates:** + +- `customer_date:'2024-03-15T14:30:00Z'` or `customer_date:>='2024-01-01'` — when the customer record was created. +- `updated_at:2024-01-01T00:00:00Z` or `updated_at:='2024-01-01'` — date the customer placed an order. Use this to check whether a customer ordered within a date range. +- `last_abandoned_order_date:'2024-04-01T10:00:00Z'`. + +**Order & spending:** + +- `orders_count:5` or `orders_count:>10` +- `total_spent:100.50` or `total_spent:>50.00` + +**Marketing:** + +- `accepts_marketing:true` + +**Tags:** + +- `tag:'VIP'` or `tag:'Wholesale,Repeat'` +- `tag_not:'Prospect'` or `tag_not:'Test,Internal'` + +**Account state (Classic Customer Accounts only):** + +- `state:ENABLED` or `state:(INVITED|DISABLED|DECLINED)` + +## Worked examples + +These combine the search syntax with Flow's date filters from [`workflow-runtime`](../workflow-runtime/SKILL.md) §Liquid. + +Orders updated in the last day (scheduled trigger): + +```liquid +updated_at:<='{{ scheduledAt }}' AND updated_at:>'{{ scheduledAt | date_minus: "1 day" }}' +``` + +Customer's recent orders. Verify `legacyResourceId` exists via `environment-paths-search` before using: + +```liquid +created_at:>'{{ "now" | date_minus: "6 months" }}' AND customer_id:{{ order.customer.legacyResourceId }} +``` + +Unfulfilled orders older than 2 days: + +```liquid +created_at:<='{{ scheduledAt | date_minus: "2 days" }}' AND fulfillment_status:unfulfilled AND NOT status:cancelled +``` + +Inactive customers (no orders in 6 months): + +```liquid +NOT order_date:>='{{ "now" | date_minus: "6 months" }}' +``` + +High-value, frequent customers: + +``` +orders_count:>=10 AND total_spent:>=1000 +``` + +## Notes and pitfalls + +- **Verify filters exist** in `task-configuration` before using. Invalid field names are silently ignored and return all results — wrong filter, wrong data, no error. +- **Prefer `legacyResourceId` over `id` when querying by ID**. Verify it exists for the object via `environment-paths-search`. If validation returns `"" is invalid. Replace this variable`, search for the correct ID field — never leave that error unfixed. +- Use ISO 8601 for dates: `2025-06-05T04:00:00.000Z`. +- Get Data actions return **0–100 resources max** per call. diff --git a/skills/shopify-flow/skills/special-tasks/SKILL.md b/skills/shopify-flow/skills/special-tasks/SKILL.md new file mode 100644 index 0000000..722e3e4 --- /dev/null +++ b/skills/shopify-flow/skills/special-tasks/SKILL.md @@ -0,0 +1,393 @@ +--- +name: special-tasks +description: "Per-task configuration quirks for Shopify Flow's special task types: scheduled triggers, Send Admin API Request, Send HTTP Request, sending emails (internal/transactional/marketing), Condition, ForEach, Error Alert, Run Code, metafields, fields with arguments (translations/inCollection), metaobjects, manual triggering, and Get Analytics Data (ShopifyQL). Load when configuring any of these." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +This skill covers the task-specific config quirks that go beyond the generic patterns in [`building-workflows`](../building-workflows/SKILL.md). For each, the pattern is: discover the task with `task-search`, read its `config_fields` via `task-configuration`, then format the values per the rules below. + +## Scheduled time trigger + +Task ID: `shopify::flow::scheduled_time`. Task type: `TRIGGER`. + +Recurrence format: + +``` +["DTSTART;TZID=:\nRRULE:"] +``` + +Components: + +- `DTSTART` — start datetime, **must be in the future**. Format: `YYYYMMDDTHHMMSS`. +- `TZID` — IANA timezone (`America/New_York`, `Europe/London`, etc.). Use `shopTimeZone` or `userTimeZone` from the IDE's `` context. +- `RRULE` — recurrence rule. + +Example — every 10 minutes starting Jun 17 2025 at 10:00 EDT: + +```json +{ + "config_field_values": [ + { + "config_field_id": "recurrence", + "value": "[\"DTSTART;TZID=America/Toronto:20250617T100000\\nRRULE:FREQ=MINUTELY;INTERVAL=10;WKST=MO\"]" + } + ] +} +``` + +`RRULE` options: + +- `FREQ`: `MINUTELY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`. +- `INTERVAL`: number of frequency units between occurrences. +- `WKST`: week start day (`MO`–`SU`). +- `BYHOUR`, `BYMINUTE`, `BYDAY`: time-of-day / day-of-week constraints. + +Constraints: + +- Never use a past `DTSTART`. +- Escape newlines as `\\n` in the JSON string. +- **Minimum interval: 10 minutes** (e.g. `FREQ=MINUTELY;INTERVAL=10`). +- **Maximum interval: 1 year.** + +## Send Admin API Request + +Task ID: `shopify::admin::admin_api_operation`. + +**Use only when no dedicated task exists.** Most common operations (tagging, updating inventory, sending emails) have purpose-built tasks with simpler config — search first via `task-search`. This task is for advanced mutations not covered by existing tasks. + +Critical rules: + +1. **Verify the mutation exists** in the Admin GraphQL API before using it. In BYO, use an Admin GraphQL-capable Shopify AI Toolkit skill/tool if one is available. Never guess mutation names. +2. **Operations needing begin/commit semantics** (Order Edit) need multiple chained requests — see worked example 7.5 in [`building-workflows`](../building-workflows/SKILL.md). + +Format — `api_call` config field value: + +```json +{ + "name": "mutationName", + "blob": "{\"param1\": \"value1\", \"param2\": \"value2\"}" +} +``` + +Rules: + +- Use the mutation **name only** (`refundCreate`, `orderEditBegin`). Do NOT include the `mutation` keyword or wrap in GraphQL syntax. +- `blob` must be valid JSON (NOT GraphQL syntax). +- Properly JSON-escape the blob inside the outer string. + +Examples (the `\n` are literal newlines inside the JSON string): + +Single parameter: + +```json +{ + "name": "orderCancel", + "blob": "{\n \"orderId\": \"{{ order.id }}\"\n}" +} +``` + +Multiple parameters: + +```json +{ + "name": "refundCreate", + "blob": "{\n \"orderId\": \"{{ order.id }}\",\n \"note\": \"Refund requested\",\n \"notify\": true\n}" +} +``` + +Nested input: + +```json +{ + "name": "customerUpdate", + "blob": "{\n \"input\": {\n \"id\": \"{{ order.customer.id }}\",\n \"tags\": [\"VIP\", \"Repeat-Buyer\"],\n \"note\": \"Updated via Flow\"\n }\n}" +} +``` + +### Chaining + +Each Admin API request response becomes a variable: `sendAdminApiRequest`, `sendAdminApiRequest1`, `sendAdminApiRequest2`. Access fields via dot: `sendAdminApiRequest.calculatedOrder.id`. + +Common pattern for Order Edit: **Begin → Modify (add/remove/discount) → Commit.** See building-workflows §7.5. + +## Send HTTP Request + +Required `config_field_values`: + +- `method` — `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`. +- `url` — full endpoint URL. +- `headers` — array as a string: `"[[\"Key1\",\"value1\"],[\"Key2\",\"value2\"]]"`. +- `body` — request payload (for `POST`/`PUT`/`PATCH`). +- `on_client_error` — `retry`, `fail`, or `ignore`. +- `on_server_error` — `retry`, `fail`, or `ignore`. + +Example POST: + +```json +{ + "config_field_values": [ + { "config_field_id": "method", "value": "POST" }, + { "config_field_id": "url", "value": "https://api.example.com/notify" }, + { "config_field_id": "headers", "value": "[[\"Content-Type\",\"application/json\"],[\"Authorization\",\"Bearer API_KEY\"]]" }, + { "config_field_id": "body", "value": "{\"order_id\":\"{{ order.id }}\",\"customer\":\"{{ order.customer.email }}\"}" }, + { "config_field_id": "on_client_error", "value": "retry" }, + { "config_field_id": "on_server_error", "value": "retry" } + ] +} +``` + +API key placeholders — when the merchant hasn't given you the actual key, use a descriptive placeholder and call it out in your reply: `YOUR_API_KEY`, `SHIPSTATION_API_KEY`, `SLACK_WEBHOOK_URL`. + +## Sending emails + +**Internal email (to shop staff):** Task ID `shopify::flow::send_email`. Configure recipients, subject, and a Liquid-rendered body. + +**Transactional email (to customers):** Search via `task-search` for "Send transactional email". Common 3rd-party publishers: FlowMail, DotDigital, FlowBuddy. Prefer `installed: true`. If none installed, tell the user they need to install the connector — do not retry. + +**Marketing email:** Task ID `shopify::email::execute_marketing_activity`. **Requires manual template configuration** in the merchant's email tool. Inform the user and stop. + +## Condition task + +```json +{ + "step_id": 2, + "task_id": "shopify::flow::condition", + "task_version": "0.1", + "task_type": "CONDITION", + "config_field_values": [ + { "config_field_id": "condition", "value": "(order.totalPrice > 100)" } + ] +} +``` + +Condition DSL syntax in [`workflow-runtime`](../workflow-runtime/SKILL.md) §Condition DSL. Output ports are `true` and `false` (NOT `output`). + +## ForEach task + +Task ID: `shopify::flow::foreach`. **Use `listpath` (NOT `list`)**: + +```json +{ + "step_id": 3, + "task_id": "shopify::flow::foreach", + "task_version": "0.1", + "task_type": "FOREACH", + "config_field_values": [ + { "config_field_id": "listpath", "value": "order.lineItems" } + ] +} +``` + +Inside the loop, items are exposed as `Foreachitem`: + +- `order.lineItems` → `lineItemsForeachitem` +- `getOrderData` → `getOrderDataForeachitem` + +Output ports: `loop_body` (for steps inside the loop) and `after` (for steps that run once after the loop completes). + +## Error alert task + +Task ID: `shopify::iris::send_shopify_alert_for_errors`. **Has hardcoded config — leave `config_field_values` empty**: + +```json +{ + "step_id": 4, + "task_id": "shopify::iris::send_shopify_alert_for_errors", + "task_version": "0.1", + "task_type": "ACTION", + "config_field_values": [] +} +``` + +If validation reports a missing config field, do NOT add config — this is an informational error; tell the user about the underlying setup requirement and stop. + +## Run Code action + +Task ID: `shopify::flow::run_code`. **Last resort.** Before reaching for it, check whether the problem can be solved with: + +1. The condition DSL (see [`workflow-runtime`](../workflow-runtime/SKILL.md) §Condition DSL) for boolean logic. +2. The Sum task (`shopify::flow::sum`) or Count task (`shopify::flow::count`) for aggregation. +3. A directly available environment path discovered via `environment-paths-search`. + +Use Run Code only when none of the above can express what's needed (e.g. filter-then-aggregate by a tag value). + +Three required config fields: + +- `input` — a **GraphQL query against the Flow environment** (NOT the Shopify Admin API). Pulls trigger / step data into the script. +- `script` — JavaScript: `export default function main(input) { ... }`. +- `output_schema` — GraphQL SDL defining the return type. + +Example `config_field_values`: + +```json +[ + { + "config_field_id": "input", + "value": "query {\n order {\n note\n lineItems {\n title\n quantity\n }\n }\n}" + }, + { + "config_field_id": "script", + "value": "export default function main(input) {\n const hasGift = input.order.lineItems.some(item => item.title.includes('Gift'));\n return { hasGiftItem: hasGift, itemCount: input.order.lineItems.length };\n}" + }, + { + "config_field_id": "output_schema", + "value": "type Output {\n \"Whether order contains a gift item\"\n hasGiftItem: Boolean!\n \"Total number of line items\"\n itemCount: Int!\n}" + } +] +``` + +Access the output via the step name: `runCode.hasGiftItem`, `runCode.itemCount`. + +Limitations: + +- No `async`/`await`. +- No `Promise`, `setTimeout`, `setInterval`. +- No `fetch` or other network I/O. +- No `import` of external modules. + +## Metafields + +**Why `patched_fields`?** Metafields are NOT automatically available in Flow. Add them to the `patched_fields` array on the workflow root to expose them as workflow variables. + +Process: + +1. Query the metafield definition with an Admin GraphQL-capable skill/tool. +2. Check `patched_fields` for an existing entry with the same arguments. +3. If found, use directly: `product.metafieldHandle.value`. +4. If not, add a new entry. + +Format: + +```json +{ + "patched_fields": [ + { + "field": "metafield", + "arguments": { "key": "loyalty_tier", "namespace": "custom" }, + "handle": "loyalty_tier", + "patched_type": "CUSTOMER", + "merchant_configured": true + } + ] +} +``` + +Supported `patched_type` values: `PRODUCT`, `CUSTOMER`, `ORDER`, `VARIANT`, etc. + +Liquid usage: + +```liquid +{{ product.customHandle.value }} +{{ customer.loyaltyTier.value }} +``` + +## Fields with arguments (generic) + +For GraphQL fields with arguments other than metafields — translations, `inCollection`, `publishedOnPublication`, etc. + +Use `environment-paths-search` and [`object-type-definition-search`](../../ops/object-type-definition-search.json) to discover field structure, arguments, and return types. Check `patched_fields` for an existing entry with the same arguments before adding a new one. For ID arguments, use `search-shop-resource` to find the entity GID. + +Format: + +```json +{ + "field": "fieldName", + "arguments": { "argName": "argValue" }, + "handle": "yourHandle", + "patched_type": "TYPE", + "merchant_configured": true +} +``` + +Translations example: + +```json +{ + "field": "translations", + "arguments": { "locale": "en" }, + "handle": "englishTranslation", + "patched_type": "PRODUCT", + "merchant_configured": true +} +``` + +```liquid +{{ product.englishTranslation.key }} +{{ product.englishTranslation.value }} +``` + +`inCollection` example: + +```json +{ + "field": "inCollection", + "arguments": { "id": "gid://shopify/Collection/456789" }, + "handle": "inSummerSale", + "patched_type": "PRODUCT", + "merchant_configured": true +} +``` + +```liquid +{{ product.inSummerSale }} +``` + +## Metaobjects + +**Metaobject triggers:** + +1. Use `search-shop-resource` to get the metaobject definition. +2. Use the **GID of the definition** in the trigger config. + +**Get Metaobject Entry:** for a specific instance, use the **GID of the definition** + the **handle of the instance**. + +**Get Metaobject Entries:** for all entries, use the **GID of the definition** only. + +## Manually triggering workflows + +Some triggers support manual fire from the admin UI: `order_created`, `draft_order_created`, `product_created`, `customer_created`. + +How: resource page → select item → "More actions" → "Run Flow Automation" → choose workflow. + +Useful to flag in your reply when the merchant might want to test before enabling the trigger. + +## Get Analytics Data (ShopifyQL) — gated + +This action is gated on the `f_flow_execute_shopifyql` verdict flag. If your environment has it enabled: + +Task ID: `shopify::flow::execute_shopifyql`. + +**Never write your own ShopifyQL query without schema support.** In BYO, the user is expected to provide the query, or you compose it from the user's request and the Admin Analytics schema. Use the query exactly as provided — preserve whitespace and newlines. + +After generating the query, ALWAYS call [`shopifyql-query-fields`](../../ops/shopifyql-query-fields.json) with that query to get the available column names: + +```bash +scripts/call_tool.mjs --op shopifyql-query-fields \ + --store \ + --arguments '{"query":""}' +``` + +The response shows `columns: [{name, type}]` — these are the only fields you can reference on each row. + +### Step name → variable + +"Get analytics data" → `getAnalyticsData`. Multiple instances: `getAnalyticsData1`, `getAnalyticsData2`. + +The step returns an object with key `rows` (array). Each item in `rows` has the columns from `shopifyql-query-fields` as fields. + +```liquid +{% for rows_item in getAnalyticsData.rows %} + {{ rows_item.product_title }} + {{ rows_item.quantity_ordered }} +{% endfor %} +``` + +Rules: + +- **Never use fields not returned by `shopifyql-query-fields`.** +- **Never access `rows` with `.first`, `.last`, or `[]`** — use Liquid filters or a `for` loop. + - Bad: `rows.first` + - Good: `rows | last` +- **Never treat `rows` as a single object.** It is always an array. diff --git a/skills/shopify-flow/skills/using-flow/SKILL.md b/skills/shopify-flow/skills/using-flow/SKILL.md new file mode 100644 index 0000000..d3862f1 --- /dev/null +++ b/skills/shopify-flow/skills/using-flow/SKILL.md @@ -0,0 +1,135 @@ +--- +name: using-flow +description: "Bootstrap skill for working with Shopify Flow from a BYO IDE host. Load whenever the user mentions Shopify Flow, workflows, triggers, conditions, actions, scenarios, runs, templates, or test events. Establishes the glossary, the Shopify CLI transport rule, the tool catalog, the never-fabricate discipline, and which other skills to load for which kind of work." +metadata: + author: Shopify Flow + version: "0.3.0" +--- + +You are helping a developer build, edit, debug, or reason about Shopify Flow workflows from their IDE. Flow is Shopify's automation product: a workflow is a directed acyclic graph of steps, started by a trigger, that runs against a shop's data. + +## How you actually do anything + +Every Flow Superpowers tool call goes through the bundled script: + +```bash +scripts/call_tool.mjs --op --store --arguments '' +``` + +Script paths are relative to this skill directory, matching the Shopify AI Toolkit convention. +Use `--arguments-file ` for large workflow payloads. + +The script invokes Shopify CLI. +Never call Flow's HTTP API directly with `curl`. Never invent or paste a bearer token. Never decide which backend implements a tool. +The script and CLI own that boundary. + +Do not teach users to scrape Shopify CLI session files. The CLI owns session lookup. + +## When to invoke which command + +- **Starting any Flow task** → make sure you have a target `--store `. If the user has not given a shop domain and no host context provides one, ask. +- **Calling a known Flow op** → use `scripts/call_tool.mjs --op --store --arguments='...'`. +- **Discovering what tools exist** → read the checked-in `ops/*.json` catalog. Each op file contains the agent-facing purpose, tool name, and argument schema. +- **Targeting a specific shop** → pass `--store `. +- **Doing work the catalog does not cover** → do not fall back to raw Flow GraphQL. Use the relevant Shopify AI Toolkit/Admin GraphQL skill if one is installed, or ask for the missing capability to be added to the Flow tool catalog. + +## Named ops + +The bundle ships checked-in JSON specs for common operations. Use the `tool` field from the matching op file. + +| Op | Purpose | +|---|---| +| `template-search` | Search Flow templates by business goal. **Run before building any workflow from scratch.** | +| `task-search` | Find tasks by natural-language description. Step 1 of task discovery. | +| `task-configuration` | Get full config schema for one or more tasks. Step 2 — source of truth for `config_field_id` values. | +| `workflow-lookup` | Read a workflow's full JSON definition by ID. Use before updating. | +| `workflow-create-or-update` | Create new or update existing workflow from JSON. | +| `environment-paths-search` | Discover Flow environment field paths. **Verify every path before using it.** | +| `search-shop-resource` | Resolve a merchant resource by name to a GID. | +| `object-type-definition-search` | Inspect a GraphQL type's field structure and arguments — for `patched_fields`. | +| `shopifyql-query-fields` | Get column names a ShopifyQL query produces. | + +The list can shift. Inspect `ops/*.json` to see what is actually shipped. + +Example: + +```bash +scripts/call_tool.mjs --op template-search \ + --store mystore.myshopify.com \ + --arguments '{"search_queries":["fraud prevention","high risk orders"]}' +``` + +The output is JSON. Read it before continuing. When the response contains errors, surface the embedded message to the user; do not paper over it. + +## The discipline + +This rule is load-bearing. Every other Flow skill depends on it. + +> **Never invent workflow IDs, task IDs, step IDs, environment paths, scope strings, trigger handles, condition expressions, or field names. If you are not sure, run a tool.** + +Concretely: + +- Do not return a workflow ID to the user that did not come back from a tool response in this session. +- Do not write a trigger payload field that you have not seen in the trigger or task schema. +- Do not autocomplete a Liquid path you have not seen in the workflow's environment for that step. Use `environment-paths-search`. +- If you only have a partial answer from real data and a confident guess fills the gap, mark the guess explicitly and ask the user to confirm. + +The single most common failure mode in Flow work is plausible fabrication: the model writes JSON that looks right but references a field that does not exist, and the merchant ships a workflow that silently misfires. The cost of running one extra tool is tiny. The cost of a hallucinated field name is a broken automation. + +## Read-only versus mutating calls + +- **Read-only** (`template-search`, `task-search`, `task-configuration`, `workflow-lookup`, `environment-paths-search`, `search-shop-resource`, `object-type-definition-search`, `shopifyql-query-fields`): run the call. No confirmation needed. +- **Mutating** (`workflow-create-or-update`, firing test events, activating/deactivating a workflow): summarize what you are about to do, including the exact CLI command and arguments, and ask the user to confirm before running. + +## Which skill for which kind of work + +Always-loaded with this skill: + +- [`flow-best-practices`](../flow-best-practices/SKILL.md) — response format, when to inform the user, when to use placeholders. + +Load on demand based on the task: + +- **Building or editing a workflow** → [`building-workflows`](../building-workflows/SKILL.md). +- **Authoring step internals (env vars, conditions, Liquid)** → [`workflow-runtime`](../workflow-runtime/SKILL.md). +- **Configuring a Get Data task or Shopify search query** → [`shopify-search-syntax`](../shopify-search-syntax/SKILL.md). +- **Configuring a special task type** (scheduled trigger, Send Admin API Request, Send HTTP Request, sending email, ForEach, Run Code, metafields, fields with arguments, metaobjects, ShopifyQL) → [`special-tasks`](../special-tasks/SKILL.md). +- **Test events / scenarios** → [`running-test-events`](../running-test-events/SKILL.md). + +## Glossary + +- **Workflow** — a single named automation. Has a current workflow definition and a list of past versions. +- **Workflow definition** — the actual graph: steps + links + a state (`MAIN`, `DRAFT`, `DEPRECATED`). +- **Step** — a node in the graph. Carries workflow-specific configuration for a task. +- **Task** — workflow-independent unit of work. Flavours: `trigger`, `condition`, `action`, `wait`, `fetch`, `for-each`. A task added to a workflow becomes a step. +- **Trigger** — the entry point. Subscribes the workflow to an event stream and brings shop/event data into the environment. +- **Condition** — a branching step. Evaluates an expression and routes to one of several outputs. +- **Action** — a step that does something, such as sending email, calling Admin API, or running Liquid-templated code. +- **Link** — an edge from one step's output port to another step's input port. +- **Activation** — the on/off state for a workflow definition in some scope, usually a shop. +- **Workflow run** — one execution of a workflow against one event. +- **Step run** — one step's execution within a workflow run. +- **Event** — the thing that fires a trigger. Sources include Shopify webhooks, core Kafka, partner apps, and manual triggering. +- **Scenario / test event** — a trigger payload fired through a workflow during development. Load [`running-test-events`](../running-test-events/SKILL.md). +- **Connector** — a collection of triggers and actions integrating an external system. +- **Liquid in Flow** — Liquid templates evaluated against the current step's environment. Restricted — see [`workflow-runtime`](../workflow-runtime/SKILL.md). +- **Patched field** — a metafield or argument-bearing field declared at the workflow root so it is exposed as an environment variable. + +## Flow phases + +1. **Understand** — what trigger? What is the merchant trying to do? What edge cases matter? +2. **Inspect** — what does the shop already have? Read workflows, triggers, schemas, and available tasks before designing. +3. **Design** — sketch trigger → conditions → actions. Name the variables needed at each branch. +4. **Build** — one step at a time. After each step, re-fetch schemas/paths before configuring the next. +5. **Verify** — fire at least one test event end-to-end and read the run log. Static review is not verification. +6. **Ship** — turn the workflow on, but leave a recovery path. + +## What never to do + +- Never call Flow's API directly for a Superpowers tool. +- Never invent IDs, field names, enum values, or condition expressions. Run a tool. +- Never declare a workflow "done" without firing at least one test event. +- Never run a mutation without summarising and confirming first. +- Never paper over an error from `scripts/call_tool.mjs`. Surface the message and stop. +- Never use Title Case for `workflow_name`. Sentence case only. +- Never use Liquid syntax inside the condition DSL. +- Never reference a `Foreachitem` outside the `loop_body` of its ForEach. diff --git a/skills/shopify-flow/skills/workflow-iac/SKILL.md b/skills/shopify-flow/skills/workflow-iac/SKILL.md new file mode 100644 index 0000000..10a5650 --- /dev/null +++ b/skills/shopify-flow/skills/workflow-iac/SKILL.md @@ -0,0 +1,158 @@ +--- +name: workflow-iac +description: Infrastructure-as-Code lifecycle for Shopify Flow workflows — local JSON files committed to git, validated against a shop, pushed and activated explicitly via the Shopify CLI. +--- + +This sub-skill covers the full IaC lifecycle: bootstrap → edit → validate → push → diff → activate. Load it when the user wants to track workflows in git, manage them as code, or iterate on a workflow file before pushing. + +## Project model + +A Flow IaC project is rooted at a directory containing `flow.toml`. Each workflow lives in its own subdirectory under `workflows/`: + +``` +my-flow-project/ + flow.toml + workflows/ + high-value-orders/ + workflow.flow.json # source of truth, edited and committed + workflow.flow.lock.json # last-pushed metadata, committed + # optional: notes.md, sample-events/, fixtures, etc. + welcome-email/ + workflow.flow.json + workflow.flow.lock.json +``` + +The per-workflow folder gives you a workspace: drop notes, sample event JSON, screenshots, or anything else relevant alongside the workflow without cluttering the project root. + +`flow.toml`: + +```toml +store = "shop.myshopify.com" + +[workflows] +dir = "workflows" +``` + +Lifecycle commands read `flow.toml` from the current directory or any ancestor. `--store` and `--workflows-dir` flags override the file when needed. + +The lockfile records `workflow_id`, `workflow_definition_version`, `payload_sha256`, `store`, and `pushed_at`. Treat it as authoritative for the lifecycle commands — it tells `activate`, `diff`, `status`, and `push` which workflow on the shop the file maps to. + +## Bootstrap (existing shop → IaC project) + +Use this when adopting IaC on a shop that already has workflows in the UI: + +```bash +shopify flow init --store shop.myshopify.com +shopify flow workflow pull --all +git add flow.toml workflows/ +git commit -m "Bootstrap Flow IaC from shop.myshopify.com" +``` + +`pull --all` calls `list-workflows` to enumerate every workflow on the shop, then writes one file per workflow to the configured directory. Pass `--include-hidden` to include hidden workflows. Existing files are skipped unless `--force` is passed. + +## Lifecycle + +```bash +# 1. Edit workflows/high-value-orders/workflow.flow.json +# The agent edits the JSON directly. Same shape as workflow-create-or-update tool args. + +# 2. Validate against the shop (no writes): +shopify flow workflow validate workflows/high-value-orders/workflow.flow.json --store shop1.my.shop.dev + +# 3. Push (creates or updates, writes/refreshes the lockfile): +shopify flow workflow push workflows/high-value-orders/workflow.flow.json --store shop1.my.shop.dev + +# 4. Inspect drift between local and remote: +shopify flow workflow diff workflows/high-value-orders/workflow.flow.json --store shop1.my.shop.dev +# Exit 0 = clean, exit 1 = differences + +# 5. Activate the pushed definition: +shopify flow workflow activate workflows/high-value-orders/workflow.flow.json --store shop1.my.shop.dev + +# 6. Deactivate when needed: +shopify flow workflow deactivate workflows/high-value-orders/workflow.flow.json --store shop1.my.shop.dev +``` + +## What the commands do under the hood + +- **validate** — sets `X-Shopify-Is-Eval: true` on the upsert tool call. Same code path as `push`, but no DB writes. Returns shop-scoped validation errors. +- **push** — calls the upsert tool with `hidden: false` so the workflow is immediately visible/activatable. Writes a fresh lockfile with the returned `workflow_id` and `workflow_definition_version`. +- **pull** — fetches a single workflow, normalizes the JSON (sorted keys, 2-space indent, trailing newline), writes the file and lockfile. With `--all`: enumerates every workflow on the shop and writes one folder per workflow. +- **show** — prints a remote workflow's normalized JSON to stdout. No files written. Use to inspect a remote workflow without committing to a local copy. +- **diff** — pulls the remote into memory, normalizes both sides, prints unified diff (`--- remote`, `+++ local`). Added lines (`+`) = what `push` would change. +- **activate / deactivate** — resolves `workflow_id` and `workflow_definition_version` from the lockfile by default. +- **status** — walks the workflows directory and classifies every file: `clean` (matches remote), `drifted` (local ≠ remote), `new` (no lockfile yet), `orphaned` (lockfile but remote missing). Also flags `unknown` workflows that exist on the shop but aren't tracked locally. Exit 1 if anything other than `clean`. + +Most lifecycle commands accept `--store`, but if a `flow.toml` is present in cwd or any ancestor, `--store` falls back to the `store` field in that file. Run inside a project and you usually don't need to pass it. + +## Activation input modes + +```bash +# (a) lockfile-driven (default) +shopify flow workflow activate workflows/high-value-orders/workflow.flow.json --store shop.myshopify.com + +# (b) explicit id + version (no lockfile required) +shopify flow workflow activate \ + --workflow-id 01HQK... \ + --workflow-version 01HQL... \ + --store shop.myshopify.com + +# (c) latest main version via lookup +shopify flow workflow activate \ + --workflow-id 01HQK... \ + --use-latest \ + --store shop.myshopify.com +``` + +`--workflow-id` alone is rejected — pass `--workflow-version` for an exact pin or `--use-latest` to resolve via workflow_lookup. + +## Hidden workflow gotcha + +Workflows created from the UI default to visible. Workflows created via Sidekick chat (the upsert tool path without IaC) default to `hidden: true` and **cannot be activated until unhidden**. The lifecycle `push` command always sends `hidden: false` — this is the IaC default and is what you want. + +If you encounter `WORKFLOW_HIDDEN` from `activate`, it means the workflow was previously created hidden (chat path or pre-IaC). Run `push` again on the file to flip `hidden: false`, then activate. + +## Format normalization + +Both `push` and `pull` write files in canonical form: keys sorted alphabetically (recursively), 2-space indent, trailing newline. This means `git diff` reflects real workflow changes rather than formatting noise. Don't reformat by hand — let the CLI normalize. + +## Worked example: end-to-end + +```bash +# Once flow.toml is initialized, --store is read from it. You can omit --store +# from every command below if cwd is inside the project. +shopify flow init --store shop1.my.shop.dev + +# Initial scaffold from a template +shopify flow tool call template-search --arguments '{"search_queries":["tag high-value orders"]}' + +# Save the chosen template's workflow_json into a local file +# workflows/high-value-orders/workflow.flow.json + +shopify flow workflow validate workflows/high-value-orders/workflow.flow.json +# → Workflow is valid. + +shopify flow workflow push workflows/high-value-orders/workflow.flow.json +# → Pushed workflow 01HQK... (version 01HQL...). Lockfile updated. + +git add workflows/high-value-orders/ +git commit -m "Add high-value-orders workflow" + +shopify flow workflow activate workflows/high-value-orders/workflow.flow.json +# → Activated workflow 01HQK... (version 01HQL...). + +shopify flow workflow status +# → clean: 1, drifted: 0, new: 0, orphaned: 0, unknown: 0 +``` + +After future edits to the JSON: `validate` → fix any errors → `push` → `diff` (should be clean) → optionally `activate` if workflow needs the new definition active. + +## Rules + +- Commit both `workflow.flow.json` and `workflow.flow.lock.json` per workflow folder. Commit `flow.toml`. +- Never hand-edit the lockfile; let `push` and `pull` write it. +- `validate` before `push` for fast feedback. Activation runs validation again, but earlier is cheaper. +- When two people edit the same workflow file in parallel, expect git conflicts on the JSON, not on the lockfile (each push refreshes it). +- If a merchant edits the workflow in the Flow UI after a push, the next `diff` (or `status`) will show drift. Decide whether to `pull` (accept their edits) or `push` (overwrite back to your local truth). +- Run `status` in CI to fail on drift between repo and shop. +- Never confirm a `push` or `activate` without explicit user OK if the change isn't trivial. diff --git a/skills/shopify-flow/skills/workflow-runtime/SKILL.md b/skills/shopify-flow/skills/workflow-runtime/SKILL.md new file mode 100644 index 0000000..824e30d --- /dev/null +++ b/skills/shopify-flow/skills/workflow-runtime/SKILL.md @@ -0,0 +1,361 @@ +--- +name: workflow-runtime +description: "What lives inside a Flow workflow step at runtime: environment variables (trigger data, step outputs, ForEach item access), the condition DSL (operators, array iteration, null/empty rules), and Flow's restricted Liquid (allowed filters, what doesn't work, Flow-specific date/crypto filters). Load when authoring step internals — config field values, condition expressions, Liquid templates." +metadata: + author: Shopify Flow + version: "0.1.0" +--- + +This skill is for the *inside* of a step. Pairs with [`building-workflows`](../building-workflows/SKILL.md) (overall structure, JSON shape, submission) and [`special-tasks`](../special-tasks/SKILL.md) (per-task config quirks). + +## Workflow environment variables + +Every workflow has access to a `shop` object plus whatever the trigger and prior steps have hydrated. **You don't see the full environment until you query it for the path you want.** + +### How to discover paths — never guess + +```bash +scripts/call_tool.mjs --op environment-paths-search \ + --store \ + --arguments '{ + "searches":[ + {"root_type":"Order","search_term":"customer email"}, + {"root_type":"Product","search_term":"tags"} + ], + "data_type_filter":"ALL" +}' +``` + +Each result carries: + +- `search_categories` — `SCALAR`, `SCALAR_IN_LIST`, `LIST`, or `ALL`. **Read this before building a condition.** + - `SCALAR` — no lists in path, use operators directly: `(order.totalPrice > 100)`. + - `SCALAR_IN_LIST` — one list in path, iterate first: `(order.tags any? |t| (t == 'vip'))`. + - `LIST` — ends in a list of objects: `(order.lineItems any? |li| (...))`. + - `ALL` alone — multiple nested lists, nest the iteration: `(order.lineItems any? |li| (li.product.tags any? |t| (t == 'fragile')))`. +- `path_elements_types` — full type chain (e.g. `["order:Order","customer:Customer","tags:String"]`). +- `leaf_type` — the final scalar type. + +Plural names (`lineItems`, `tags`, `variants`) are a strong hint that a segment is a list — but verify. + +### Translating user words to field names + +Merchants describe things in non-technical language. Translate before searching: + +- "how much the order costs" → `totalPrice`, `subtotalPrice`, `currentTotalPrice` +- "where the customer lives" → `customer address`, `defaultAddress`, `city`, `country` +- "what they bought" → `lineItems`, `product`, `variant` +- "how many items" → `quantity`, `lineItems`, `currentQuantity` +- "discount" / "coupon" → `discountCode`, `discountApplications`, `totalDiscounts` +- "shipping" / "delivery" → `shippingLine`, `shippingAddress`, `fulfillment` +- "inventory" / "stock" → `inventoryLevel`, `inventoryQuantity`, `inventoryItem` +- "when it was placed" → `createdAt`, `processedAt` + +These are illustrative — translate based on context. + +### Trigger data hydration + +When a trigger fires, Flow automatically loads relevant fields from the event. **You usually do NOT need a Get Data step for the same object that triggered the workflow.** + +Trigger data uses **camelCase** object names: `order.customer.email`, `fulfillmentOrder.id`, `draftOrder.id`. + +Use Get Data only when: +- You need related records not in the trigger payload (e.g. all customer orders when triggered by a single order). +- You need additional context (e.g. product inventory when triggered by an order). +- You need historical or aggregate data beyond the triggering event. + +Do **not** Get Data for the trigger object itself — `order` is already there when triggered by `order_created`. + +### Step output variables + +Each step adds a variable to the environment, named by camelCasing the step name. Multiple instances of the same step type get suffixes. + +| Step | Variable name | Shape | +|---|---|---| +| "Get order data" | `getOrderData` | array (filterable list) | +| Multiple Get Data | `getOrderData`, `getOrderData1`, `getOrderData2` | each its own array | +| ForEach over `order.lineItems` | `lineItemsForeachitem` (inside body only) | one item per iteration | +| ForEach over `getOrderData` | `getOrderDataForeachitem` (inside body only) | one item per iteration | +| "Send admin api request" | `sendAdminApiRequest`, `sendAdminApiRequest1` | mutation/query response object | +| "Count" | `count`, `count1` | integer | +| "Sum" | `sum`, `sum1` | number | + +**ForEach iteration items are ONLY available inside the loop body.** Don't reference `lineItemsForeachitem` in a step connected to the `after` port — Flow will reject it as `foreachBodyErrors`. + +### Object access rules + +```liquid +{{ order.name }} +{{ customer.email }} +{{ getOrderData.name }} +{{ lineItemsForeachitem.title }} +``` + +Wrong (these are GraphQL connection patterns Flow doesn't expose): + +```liquid +{{ order.lineItems.edges | map: 'node' }} +{{ customer.orders.nodes }} +{{ getOrderData.edges }} +``` + +Rules: + +- Object names always **lowercase**: `order.lineItems`, never `Order.lineItems`. +- **No `.edges`, no `.nodes`, no `| map: 'node'`** — Flow handles GraphQL plumbing automatically. +- Direct property access throughout. + +## Condition DSL + +The condition DSL is **not Liquid**. It has its own grammar. + +### Data types + +| Type | Example | +|---|---| +| Integer | `42` | +| Float | `4.25` | +| String | `'paid'` (case-insensitive comparisons) | +| Boolean | `true` / `false` | +| Date | `'2025-06-05T04:00:00.000Z'` (ISO 8601) | +| Money | `10.00` | +| Decimal | `3.14159` | +| Enum | predefined constants | + +### Operators + +**Binary** (compare two values): + +- `==` `!=` `>` `>=` `<` `<=` +- `start_with?` / `not_start_with?` / `end_with?` / `not_end_with?` — **strings only** +- `include?` / `not_include?` — **strings only** (substring check). To check list membership, use `any?` instead. +- `in?` / `not_in?` — value is in a comma-separated list literal: `(order.financialStatus in? 'paid,partially_refunded,refunded')`. Numeric works too: `(order.quantity in? '1,2,3')`. + +**Unary** (check single value): + +- `empty_or_nil?` / `not_empty_and_not_nil?` — **strings only** +- `nil?` / `not_nil?` — for everything else (Integer, Float, Boolean, Date, Money, Decimal, Enum) + +**Array** (iterate a list): + +- `any?` — at least one item matches +- `all?` — every item matches +- `none?` — no item matches + +**Logical**: + +- `&&` (AND), `||` (OR) + +### Examples + +Simple: + +``` +(order.totalPrice > 100) +``` + +String operator on a string field: + +``` +(order.customer.email end_with? '.edu') +(order.note include? 'urgent') +``` + +Iterate into a list with array operators: + +``` +(order.lineItems any? |lineItems_item| (lineItems_item.product.tags any? |tags_item| (tags_item == 'presale'))) +``` + +Multiple criteria inside a list item: + +``` +(order.lineItems any? |lineItems_item| ((lineItems_item.product.tags any? |tags_item| (tags_item == 'presale')) && (lineItems_item.product.productType == 'Clothing'))) +``` + +### Null/empty checks — use the right operator for the type + +For **strings**: + +``` +CORRECT: (shop.email empty_or_nil?) +CORRECT: (customer.firstName not_empty_and_not_nil?) +WRONG: (shop.email == '') +``` + +For **non-strings** (Integer, Float, Boolean, Date, Money, Decimal, Enum): + +``` +CORRECT: (order.totalPrice nil?) +CORRECT: (order.createdAt not_nil?) +WRONG: (order.totalPrice empty_or_nil?) +WRONG: (order.createdAt not_empty_and_not_nil?) +``` + +### List operations — always iterate first + +To check if a list contains a value, use `any?` with `==` — never `include?`: + +``` +CORRECT: (order.tags any? |tags_item| (tags_item == 'vip')) +WRONG: (order.tags include? 'vip') +``` + +Applying a string operator to list items requires iteration: + +``` +CORRECT: (product.tags any? |tags_item| (tags_item start_with? 'presale')) +WRONG: (product.tags start_with? 'presale') +``` + +Checking list items for a value requires iteration: + +``` +CORRECT: (getOrderData any? |getOrderData_item| (getOrderData_item.id not_empty_and_not_nil?)) +WRONG: (getOrderData.id not_empty_and_not_nil?) +``` + +### Rules + +1. Always run `environment-paths-search` to confirm the path exists. +2. Match operators to data types. +3. **No Liquid syntax in conditions.** No `{{ }}`, no `{% %}`, no Liquid filters. The condition DSL cannot evaluate them. +4. **Never mix `&&` and `||` at the same nesting level.** If you need complex logic, use multiple condition steps. +5. Check `search_categories` from path search — `SCALAR_IN_LIST` and `LIST` paths MUST iterate before applying any operator. +6. `include?`/`not_include?`/`start_with?`/`end_with?` are strings-only. +7. `empty_or_nil?`/`not_empty_and_not_nil?` are strings-only. +8. String comparisons are case-insensitive. +9. Dates: ISO 8601, e.g. `2025-06-05T04:00:00.000Z`. +10. Booleans: `true` / `false`. + +### No Liquid in conditions — common pitfall + +WRONG (will fail at validation): + +``` +(getCustomerDataForeachitem.lastOrder.createdAt < '{{ scheduledAt | date_minus: "6 months" }}') +``` + +CORRECT options: + +- Hardcode the date: `(getCustomerDataForeachitem.lastOrder.createdAt < '2024-06-05T04:00:00.000Z')` +- Use an existing variable: `(order.createdAt > customer.lastOrderDate)` +- Compute dynamically with a Run Code action first, then reference its output. + +### Output format (in `config_field_values`) + +```json +{ + "config_field_values": [ + { "config_field_id": "condition", "value": "(order.totalPrice > 100)" } + ] +} +``` + +## Liquid in Flow + +Flow's Liquid is restricted. It only outputs **scalar values** (strings, numbers, booleans). To work with complex data, use ForEach to iterate. + +### What you cannot do + +- Output entire lists/objects: `{{ order.lineItems }}` — wrong. +- Use the `json` filter: `{{ order | json }}` — wrong. +- Use `size` on objects/lists: `{{ order.lineItems | size }}` — wrong. +- Use dot notation for filters: `{{ order.lineItems.size }}` — wrong. +- Use array indexes: `{{ order.lineItems[0].title }}` — wrong. +- Use `.first`, `.last`, `.empty?` filters — wrong. +- Use filters inside conditional comparisons: `{% if order.lineItems.id | size > 0 %}` — wrong. + +### What you can do + +- Dot notation for properties: `{{ order.customer.email }}`. +- Loop through lists: `{% for item in order.lineItems %}`. +- `size` on **field arrays** (the trick): `{{ order.lineItems.id | size }}`. This extracts the `id` field from every item then counts — Flow allows this shape. +- Access individual properties inside loops. +- Assign first, then test: `{% assign count = order.lineItems.id | size %}{% if count > 0 %}`. +- Flow-specific filters (date arithmetic, crypto hashing) below. + +### Flow-specific filters + +**Date filters** (mostly used in query filters for Get Data): + +- `date_minus` — subtract time from a date. + - `{{ "now" | date_minus: "6 months" }}` + - `{{ scheduledAt | date_minus: "1 day" }}` +- `date_plus` — add time to a date. + - `{{ "now" | date_plus: "30 days" }}` + - `{{ order.createdAt | date_plus: "1 week" }}` +- Units: `days`, `weeks`, `months`, `years`. + +**Crypto filters**: + +- `md5` `sha1` `sha256` `blake3` — hash filters: `{{ order.id | sha256 }}`. +- `hmac_sha1`, `hmac_sha256` — `{{ order.id | hmac_sha256: secret_key }}`. + +### Liquid examples + +Check payment status: + +```liquid +{% if order.financialStatus == "paid" %} + The order amount paid was {{ order.totalPrice | money }} +{% else %} + Order is not paid yet +{% endif %} +``` + +Loop through line items (note the `size` trick on `.id`): + +```liquid +{% assign item_count = order.lineItems.id | size %} +{% if item_count > 0 %} + {% for item in order.lineItems %} + Item: {{ item.title }} + Quantity: {{ item.quantity }} + Price: {{ item.price | money }} + {% endfor %} +{% else %} + No items in order +{% endif %} +``` + +Stock status with branching per item: + +```liquid +{% for item in order.lineItems %} + {{ item.title }} + Quantity: {{ item.quantity }} + Stock: {{ item.variant.inventoryQuantity }} + + {% if item.variant.inventoryQuantity <= 0 %} + Status: OUT OF STOCK + {% elsif item.variant.inventoryQuantity < item.quantity %} + Status: INSUFFICIENT STOCK + {% else %} + Status: IN STOCK + {% endif %} +{% endfor %} +``` + +Customer profile (verify paths first via `environment-paths-search`): + +```liquid +Customer Information: + +{% if order.customer.firstName or order.customer.lastName %} + Name: {{ order.customer.firstName }} {{ order.customer.lastName }} +{% endif %} + +{% if order.customer.email %} + Email: {{ order.customer.email }} +{% endif %} + +Total Orders: {{ order.customer.ordersCount }} +Total Spent: {{ order.customer.totalSpent | money }} + +{% if order.customer.ordersCount == 1 %} + [First Time Customer!] +{% elsif order.customer.ordersCount >= 10 %} + [Loyal Customer] +{% endif %} +```