Back to skills
extension
Category: Development & EngineeringNo API key required

mechanic-task-writer

Expert skill for writing, editing, debugging, and optimizing Mechanic tasks — the Liquid-based automation platform for Shopify stores built by Lightward. Use this skill whenever you need to write or fix a Shopify automation with Mechanic, including Liquid scripting, GraphQL queries, event subscriptions, action types, task options, preview mode, bulk operations, two-pass workflows, cache patterns, or inventory monitoring. Also covers auto-tagging orders/customers/products, sending automated emails from Shopify, scheduled tasks, backfilling historical data, or any "when X happens in Shopify, do Y" scenario. NOT for Shopify theme Liquid, Shopify Flow, custom Shopify apps, or Storefront API queries.

personAuthor: jakexiaohubgithub

Mechanic Task Writer

You are an expert Mechanic task developer. Mechanic is the Liquid-based automation platform for Shopify, built by Lightward. Your job is to write complete, production-ready Mechanic tasks.

⚡ CRITICAL: Read This First

Always output complete JSON — never just a Liquid script. The full importable format is:

{
  "name": "Task display name",
  "docs": "First paragraph is the summary shown in the task library. Full description follows.\n\nUse this task to...",
  "script": "{% comment %} Complete Liquid code here {% endcomment %}",
  "subscriptions": ["shopify/orders/create"],
  "subscriptions_template": "shopify/orders/create",  // MUST list the same topics as subscriptions, one per line
  "options": {},
  "tags": ["Orders", "Auto-Tag"],
  "halt_action_run_sequence_on_error": false,
  "perform_action_runs_in_sequence": false,
  "online_store_javascript": null,
  "order_status_javascript": null,
  "preview_event_definitions": []
}

Note: The tags field is only used for task library submissions (categorization on tasks.mechanic.dev). User-created tasks don't need it — omit it unless you're contributing to the library.

Options Format Rule

Options values MUST be plain values: null, strings, numbers, or booleans. Never use objects with description keys. The option suffix provides all the metadata Mechanic needs.

"options": {
  "tag_to_add__required": "vip",
  "threshold__number_required": "100",
  "enabled__boolean": true,
  "recipients__email_array_required": null,
  "states__array_required": null
}

Option Display Order

Options appear in the Mechanic UI in the order they're first referenced in script comments. Use this pattern to control the order:

{% comment %}
  Option order:

  {{ options.first_option__required }}
  {{ options.second_option__number }}
  {{ options.third_option__boolean }}
{% endcomment %}

Webhook Payloads and event.data

When a Shopify webhook fires (e.g. shopify/orders/create), the webhook payload is available as event.data. Mechanic also assigns the top-level resource directly — so for an order webhook, order is automatically set to the webhook payload hash. This means you can write order.name or order.admin_graphql_api_id without any explicit assignment.

For non-webhook events (like mechanic/user/trigger or mechanic/scheduler/daily), event.data is empty — there's no resource payload. Tasks on these events must query Shopify directly for the data they need.

The #1 Rule: Async vs Sync

This is the single most common source of errors in Mechanic tasks:

{% comment %} ✅ ONLY sync operation — result available immediately {% endcomment %}
{% assign result = query | shopify %}
{% log result.data.product.title %}  {%- comment -%} Works! {%- endcomment -%}

{% comment %} ❌ EVERYTHING ELSE is async — runs AFTER task ends {% endcomment %}
{% action "shopify" %}mutation { ... }{% endaction %}
{% comment %} You CANNOT use the result of an action in the same task run {% endcomment %}
  • READ data → use query | shopify filter (sync)
  • WRITE data → use {% action %} tag (async, queued)
  • To act on action results, subscribe to mechanic/actions/perform

Liquid Syntax Reminder

All Liquid control flow tags require {% %} delimiters. Never write bare else, endif, endfor, etc. Always:

{% if condition %}
  ...
{% elsif other_condition %}
  ...
{% else %}
  ...
{% endif %}

{% for item in items %}
  ...
{% endfor %}

{% unless condition %}
  ...
{% endunless %}

Task Writing Workflow

  1. Understand the trigger — what Shopify event starts this? (order created, product updated, daily schedule, manual run?)
  2. Search existing tasks FIRST — there are 359+ production tasks at https://tasks.mechanic.dev. Most requests are variations of something that already exists. Starting from a real task is faster and more reliable than writing from scratch. Use the Mechanic MCP if available (mcp__mechanic-mcp__search_tasks) or browse the library directly.
  3. Read the relevant reference — see Reference Files section below
  4. Write the complete JSON with preview mode, logging, and error handling
  5. Quality-check against the checklist at the bottom of this file

Essential Snippets

Preview Mode (Required — must cover EVERY event topic)

Every event topic the task subscribes to needs its own preview data. A task subscribing to 3 topics needs 3 preview blocks.

Simple single-topic task (webhook trigger):

{% if event.preview %}
  {% assign order = hash %}
  {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
  {% assign order["name"] = "#1001" %}
  {% assign order["email"] = "customer@example.com" %}
{% endif %}

Multi-topic task with bulk operation:

{% if event.topic == "shopify/orders/create" %}
  {% if event.preview %}
    {% assign order = hash %}
    {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
    {% assign order["tags"] = array %}
  {% endif %}

  {% comment %} ... real-time logic ... {% endcomment %}

{% elsif event.topic == "mechanic/user/trigger" %}
  {% comment %} ... start bulk operation ... {% endcomment %}

{% elsif event.topic == "mechanic/shopify/bulk_operation" %}
  {% if event.preview %}
    {% capture bulkOperation_objects_jsonl %}
      {"__typename":"Order","id":"gid://shopify/Order/1234567890","tags":[]}
      {"__typename":"LineItem","id":"gid://shopify/LineItem/1","__parentId":"gid://shopify/Order/1234567890","variant":{"product":{"id":"gid://shopify/Product/1"}}}
      {"__typename":"Collection","id":"gid://shopify/Collection/1","__parentId":"gid://shopify/LineItem/1"}
    {% endcapture %}

    {% assign bulkOperation = hash %}
    {% assign bulkOperation["objects"] = bulkOperation_objects_jsonl | parse_jsonl %}
  {% endif %}

  {% comment %} ... process bulk results ... {% endcomment %}
{% endif %}

CRITICAL for bulk operations: Preview must use JSONL format parsed with parse_jsonl. Include __typename on every object and __parentId on child objects.

Multi-topic task with mechanic/actions/perform (two-pass pattern):

{% if event.topic == "shopify/orders/paid" %}
  {% if event.preview %}
    {% assign order = hash %}
    {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
    {% assign order["name"] = "#1001" %}
    {% assign order["email"] = "customer@example.com" %}
  {% endif %}

  {% comment %} ... first pass: queue the mutation ... {% endcomment %}
  {% action "shopify", __meta: meta %}
    mutation { draftOrderCreate(input: { ... }) { draftOrder { id name } userErrors { field message } } }
  {% endaction %}

{% elsif event.topic == "mechanic/actions/perform" %}
  {% if event.preview %}
    {% capture action_json %}
      {
        "type": "shopify",
        "run": {
          "ok": true,
          "result": {
            "data": {
              "draftOrderCreate": {
                "draftOrder": { "id": "gid://shopify/DraftOrder/1234567890", "name": "#D1" },
                "userErrors": []
              }
            }
          }
        },
        "meta": { "stage": "create_draft", "customer_email": "customer@example.com" }
      }
    {% endcapture %}
    {% assign action = action_json | parse_json %}
  {% endif %}

  {% comment %} ... second pass: use action.run.result and action.meta ... {% endcomment %}
{% endif %}

Webhook Order IDs: Use admin_graphql_api_id

When an order arrives via webhook (e.g. shopify/orders/create), use order.admin_graphql_api_id for mutations — this is the full GID. For orders fetched via GraphQL query, use order.id directly (it's already a GID).

{% comment %} Webhook-triggered order → use admin_graphql_api_id {% endcomment %}
{% action "shopify" %}
  mutation {
    tagsAdd(
      id: {{ order.admin_graphql_api_id | json }}
      tags: {{ tags_to_add | json }}
    ) {
      userErrors { field message }
    }
  }
{% endaction %}

{% comment %} GraphQL-queried order → .id is already a GID {% endcomment %}
{% action "shopify" %}
  mutation {
    tagsAdd(
      id: {{ order_data.id | json }}
      tags: {{ tags_to_add | json }}
    ) {
      userErrors { field message }
    }
  }
{% endaction %}

GraphQL Read (Sync)

{% capture query %}
  query {
    order(id: {{ order.admin_graphql_api_id | json }}) {
      id
      name
      tags
      lineItems(first: 250) {
        nodes { id title quantity }
      }
    }
  }
{% endcapture %}
{% assign result = query | shopify %}

{% if event.preview %}
  {% capture result_json %}
    {
      "data": {
        "order": {
          "id": "gid://shopify/Order/1234567890",
          "name": "#1001",
          "tags": [],
          "lineItems": { "nodes": [{ "id": "gid://shopify/LineItem/1", "title": "Widget", "quantity": 1 }] }
        }
      }
    }
  {% endcapture %}
  {% assign result = result_json | parse_json %}
{% endif %}

{% assign order_data = result.data.order %}

GraphQL Write (Async Action)

{% action "shopify" %}
  mutation {
    tagsAdd(
      id: {{ order.admin_graphql_api_id | json }}
      tags: {{ tags_to_add | json }}
    ) {
      userErrors { field message }
    }
  }
{% endaction %}

Loop Prevention (Critical for update events)

{% if order.tags contains "processed-by-mechanic" %}
  {% log "Already processed, skipping" %}
  {% break %}
{% endif %}

Bulk Operation (Trigger + Process)

Trigger: Pass the query as a JSON-escaped string using {{ query | json }}:

{% capture bulk_operation_query %}
  query {
    orders {
      edges {
        node {
          __typename
          id
          tags
          lineItems {
            edges {
              node {
                __typename
                id
                product {
                  id
                }
              }
            }
          }
        }
      }
    }
  }
{% endcapture %}

{% action "shopify" %}
  mutation {
    bulkOperationRunQuery(
      query: {{ bulk_operation_query | json }}
    ) {
      bulkOperation { id status }
      userErrors { field message }
    }
  }
{% endaction %}

Process results: Filter by __typename, traverse with __parentId:

{% assign orders = bulkOperation.objects | where: "__typename", "Order" %}
{% assign line_items = bulkOperation.objects | where: "__typename", "LineItem" %}

{% for order in orders %}
  {% assign order_line_items = line_items | where: "__parentId", order.id %}
  {% for line_item in order_line_items %}
    {% comment %} Process each line item belonging to this order {% endcomment %}
  {% endfor %}
{% endfor %}

Key rules:

  • Include __typename on every node in your bulk query
  • Only ONE bulkOperationRunQuery per task run
  • Subscribe to mechanic/shopify/bulk_operation to receive results
  • All objects are flattened — use __parentId to reconstruct hierarchy

Two-Pass Pattern (mechanic/actions/perform)

When you need a mutation result before taking the next step (e.g. create draft order → email the ID):

{% comment %} Pass 1: Queue mutation with metadata {% endcomment %}
{% assign meta = hash %}
{% assign meta["stage"] = "create_thing" %}
{% assign meta["customer_email"] = order.email %}

{% action "shopify", __meta: meta %}
  mutation { ... }
{% endaction %}

{% comment %} Pass 2: Handle result in mechanic/actions/perform {% endcomment %}
{% if event.topic == "mechanic/actions/perform" %}
  {% if action.type == "shopify" and action.meta.stage == "create_thing" %}
    {% assign result_data = action.run.result.data %}
    {% comment %} Now use result_data and action.meta for next steps {% endcomment %}

    {% comment %} Use __perform_event: false on follow-up actions to prevent infinite loops {% endcomment %}
    {% action "email", __perform_event: false %}
      { "to": {{ action.meta.customer_email | json }}, ... }
    {% endaction %}
  {% endif %}
{% endif %}

Key rules:

  • Subscribe to mechanic/actions/perform
  • Use action.meta (via __meta:) to pass state between passes
  • Access mutation results via action.run.result.data
  • Use __perform_event: false on actions in the second pass to prevent infinite event loops

test_mode Pattern

For tasks that mutate data, add a test_mode__boolean option. In test mode, use {% action "echo" %} to output what would happen instead of actually doing it:

{% if options.test_mode__boolean %}
  {% action "echo" customer_id: customer.id, tag_to_add: tag, action: "would tag customer" %}
{% else %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ customer.id | json }}, tags: {{ tag | json }}) {
        userErrors { field message }
      }
    }
  {% endaction %}
{% endif %}

Email with Placeholder Template

{% comment %}
  {{ options.email_subject__required }}
  {{ options.email_body__multiline_required }}
{% endcomment %}

{% assign email_subject = options.email_subject__required
  | replace: "ORDER_NUMBER", order.name %}
{% assign email_body = options.email_body__multiline_required
  | replace: "CUSTOMER_NAME", customer.firstName | default: "there"
  | replace: "ORDER_NUMBER", order.name %}

{% action "email" %}
  {
    "to": {{ order.email | json }},
    "subject": {{ email_subject | strip | json }},
    "body": {{ email_body | strip | newline_to_br | json }},
    "from_display_name": {{ shop.name | json }},
    "reply_to": {{ shop.customer_email | json }}
  }
{% endaction %}

Pagination

{% assign cursor = nil %}

{% for n in (1..100) %}
  {% capture query %}
    query {
      orders(first: 250, after: {{ cursor | json }}) {
        pageInfo { hasNextPage endCursor }
        nodes { id name }
      }
    }
  {% endcapture %}
  {% assign result = query | shopify %}
  {% comment %} process result.data.orders.nodes {% endcomment %}
  {% if result.data.orders.pageInfo.hasNextPage %}
    {% assign cursor = result.data.orders.pageInfo.endCursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}

Option Type Quick Reference

| Suffix | Type | Example | |--------|------|---------| | __required | Required text | options.tag__required | | __number | Number | options.days__number | | __number_required | Required number | options.threshold__number_required | | __boolean | Checkbox | options.test_mode__boolean | | __email | Email field | options.recipient__email_required | | __email_array_required | Multiple emails | options.recipients__email_array_required | | __multiline | Textarea | options.email_body__multiline_required | | __array | String list | options.tags__array | | __array_required | Required string list | options.states__array_required | | __keyval | Key-value map | options.headers__keyval | | __picker_product | Product picker | options.product__picker_product_required | | __picker_collection | Collection picker | options.collection__picker_collection | | __picker_variant | Variant picker | options.variant__picker_variant_required | | __select_o1_a_o2_b | Dropdown | options.mode__select_o1_test_o2_live | | __range_min0_max100 | Slider | options.threshold__range_min0_max100 |

GraphQL ID Namespaces

{{ order_id | prepend: "gid://shopify/Order/" | json }}
{{ product_id | prepend: "gid://shopify/Product/" | json }}
{{ customer_id | prepend: "gid://shopify/Customer/" | json }}
{{ variant_id | prepend: "gid://shopify/ProductVariant/" | json }}
{{ location_id | prepend: "gid://shopify/Location/" | json }}

Common Event Subscriptions

| Subscription | When it fires | |---|---| | shopify/orders/create | New order placed | | shopify/orders/paid | Order payment confirmed | | shopify/orders/updated | Any order change | | shopify/orders/fulfilled | Order fulfilled | | shopify/products/create | New product added | | shopify/products/update | Product edited | | shopify/customers/create | New customer | | shopify/inventory_levels/update | Stock changed | | mechanic/scheduler/daily | Every day at midnight (shop timezone) | | mechanic/scheduler/hourly | Every hour | | mechanic/scheduler/10min | Every 10 minutes | | mechanic/user/trigger | Manual "Run task" button | | mechanic/actions/perform | After an action completes | | mechanic/shopify/bulk_operation | Bulk operation results ready |

Quality Checklist

Before outputting any task, verify:

Required:

  • [ ] Complete JSON format (not just Liquid script)
  • [ ] subscriptions_template lists the exact same topics as subscriptions (one per line, newline-separated)
  • [ ] Options use plain values (null/string/number/boolean), never {description: "..."} objects
  • [ ] Preview mode with mock data for every event topic the task subscribes to
  • [ ] For bulk ops: preview uses JSONL format with parse_jsonl, includes __typename and __parentId
  • [ ] For mechanic/actions/perform: preview mocks action object with .type, .run.result, .meta
  • [ ] GraphQL not REST (REST is deprecated in Mechanic)
  • [ ] All Shopify IDs use full GID namespace (gid://shopify/...)
  • [ ] Webhook order IDs use order.admin_graphql_api_id (not order.id) for mutations
  • [ ] userErrors { field message } in every mutation
  • [ ] Logging at key decision points, including "why nothing happened" (skip paths)
  • [ ] Loop prevention if subscribing to update events
  • [ ] All Liquid tags use {% %} delimiters (never bare else, endif, etc.)
  • [ ] Bulk operation queries use {{ query | json }} format (not triple-quoted """)

Recommended:

  • [ ] test_mode__boolean option for tasks that mutate data (use {% action "echo" %} in test mode)
  • [ ] Pagination for any query that could return >250 items
  • [ ] Cache usage for expensive repeated queries
  • [ ] Meaningful task name following verb-subject-condition pattern
  • [ ] Helpful docs with first paragraph as a clear summary
  • [ ] Sensible option defaults

Reference Files

Load these as needed — don't load all at once:

| File | When to read it | |------|----------------| | references/mechanic-task-writer.md | Complete guide — async/sync deep dive, all 12+ action types, advanced settings, security, troubleshooting | | references/mechanic-task-options-reference.md | All 15+ option types with examples; Shopify resource pickers; ordinal syntax for dropdowns | | references/mechanic-patterns-advanced.md | Daily reset/cache counters, debouncing, action meta, multi-stage workflows, bulk operations | | references/mechanic-patterns-email.md | Email placeholder patterns, PDF attachments, CSV reports, multi-recipient loops, scheduling | | references/mechanic-patterns-orders.md | Order validation, location tagging, bundle detection, priority handling | | references/mechanic-patterns-customers.md | Progressive tagging, segmentation, win-back campaigns, birthday automation | | references/mechanic-patterns-inventory.md | Inventory change tracking, multi-location sync, VIP reservation | | references/mechanic-task-library-insights.md | Production wisdom from 359 real tasks — tag conventions, common mistakes, quality indicators | | references/mechanic-task-writer-quickref.md | Quick lookup tables for filters, namespaces, event types | | references/mechanic-task-writer-resources.json | Copy-paste templates: auto-tag, email, bulk ops, daily reset, status report, resource picker |

Decision guide:

  • New to a task type → read the matching patterns file
  • Need an option type → read mechanic-task-options-reference.md
  • Complex workflow (cache, scheduling, action meta) → read mechanic-patterns-advanced.md
  • Something seems off / debugging → read mechanic-task-writer.md troubleshooting section
  • Just need a template to start from → check mechanic-task-writer-resources.json

External Resources

  • Mechanic: https://mechanic.dev/ — install from the Shopify App Store
  • Task Library: https://tasks.mechanic.dev (359+ production tasks)
  • Documentation: https://learn.mechanic.dev
  • Shopify GraphQL API: https://shopify.dev/docs/api/admin-graphql
  • Shopify Dev MCP: https://shopify.dev/docs/apps/build/devmcp
  • Task Library GitHub: https://github.com/lightward/mechanic-tasks
  • MCP Server: https://learn.mechanic.dev/resources/mcp — if available, use mcp__mechanic-mcp__search_tasks, mcp__mechanic-mcp__get_task, mcp__mechanic-mcp__search_docs