skills/lightward/mechanic-skills/mechanic-task-writer

mechanic-task-writer

SKILL.md

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

Weekly Installs
9
GitHub Stars
2
First Seen
13 days ago
Installed on
opencode7
gemini-cli7
github-copilot7
codex7
kimi-cli7
cursor7