# Introduction

Mechanic is a Shopify automation platform — hundreds of ready-to-use tasks plus a Liquid development environment for custom workflows.

Mechanic is a Shopify automation and development platform. Write Liquid code that responds to store events, queries the Shopify Admin API, calls external services, and performs complex multi-step workflows — with no servers to manage.

> I'm glad you're here. :) –Isaac

* [Browse the task library](https://tasks.mechanic.dev/) — hundreds of ready-to-use automations for tagging, emails, inventory, fulfillment, and more
* [Get started](/getting-started) — install Mechanic and set up your first task in minutes
* [Build something custom](/custom) — create a task tailored to your store's workflow
* [Join our Slack community](/resources/slack) — get help and share ideas with other merchants and developers

{% hint style="info" %}
Find Mechanic on the Shopify App Store: [apps.shopify.com/mechanic](https://apps.shopify.com/mechanic)
{% endhint %}

## Can Mechanic help me?

Mechanic gives you a task library with hundreds of ready-to-use automations — and a full development environment for building your own. Tasks are real code: Liquid with direct access to Shopify's GraphQL API, external HTTP services, and composable event-driven workflows. Here's how to tell if it fits your use case.

1. Are you working on something Shopify-related?
   * Mechanic is only available for Shopify.
2. Is what you're looking for already available from [Mechanic's task library](https://tasks.mechanic.dev/)?
   * We have hundreds of common scenarios already handled with pre-written, open-source, modifiable, off-the-shelf tasks.
3. Is what you're trying to do something Shopify supports?
   * Mechanic works through Shopify's own systems. If Shopify can do it, Mechanic can usually automate it. ([Here's what Shopify's Admin API covers.](https://shopify.dev/docs/api/admin-graphql))
4. Need something tailored to your store?
   * You can [build a custom task](/custom) yourself, or [hire a developer](/hire-a-developer) to create one for you. Either way, the path is well-established.

That list wasn't exactly a formal flowchart, but we hope it's helpful as you're evaluating Mechanic. :) The short version: Mechanic handles the workflows that are too complex for built-in Shopify tools, backed by a community that has collectively solved hundreds of distinct automation problems. (Join our [community Slack workspace](/resources/slack)!)

{% hint style="info" %}
Got a question you need answered now? [Join our Slack workspace.](https://join.slack.com/t/usemechanic/shared_invite/zt-cq84nrs7-ggYbYTbf~CrCjTg8nmHP2A) 💬
{% endhint %}

## How does Mechanic work?

### Tasks, events, and actions

A developer writes [**tasks**](/core/tasks) – Liquid code that responds to [**events**](/core/events), like a Shopify webhook, a manual trigger, a regular interval (e.g. hourly, daily), or an incoming email. Tasks use [**subscriptions**](/core/tasks/subscriptions) to signal their interest in specific event types.

When a task receives an incoming event, it can choose to generate an [**action**](/core/actions) – an operation that has an effect.

* The [Shopify](/core/actions/shopify) action makes changes to a Shopify store, like tagging, publishing, creating or deleting resources. It provides direct access to Shopify's Admin API. Use GraphQL for new Shopify automation work.
* The [Email](/core/actions/email) action is for sending email. It supports custom templates, and attachments.
* The [FTP](/core/actions/ftp) action is for uploading files to an FTP or SFTP server. These files may be generated by the task, or can be fetched from external locations.
* The [HTTP](/core/actions/http) action performs any request, to any HTTP endpoint. This facilitates integration with third-party APIs.
* The [Files](/core/actions/files) action generates a variety of file formats, including PDF, CSV, ZIP, and anything retrieved from a public URL. Files generated this way receive a temporary URL of their own, and can be fed into other tasks for further processing.

For a complete list of supported actions, see [Actions](/core/actions).

### Liquid

Tasks are written in [**Liquid**](/platform/liquid/basics) – the template language created by Shopify. But Mechanic extends Liquid well beyond templating: tasks can loop through paginated API results, transform complex data structures, branch on conditions, emit events that trigger other tasks, and construct arbitrarily complex action sequences. In the same way that a Liquid theme receives browser requests and renders HTML, a Mechanic task receives events and renders actions (by defining them with JSON).

In Mechanic, Liquid is extended with filters for [parsing JSON, generating CSV, computing HMACs](/platform/liquid/filters), manipulating complex data structures — including first-class support for [arrays](/platform/liquid/basics/types#array) and [hashes](/platform/liquid/basics/types#hash) — and using shop-level [globals and secrets](/platform/globals-and-secrets) for reusable task configuration.

### Run queues

Mechanic performs work using queues of [**runs**](/core/runs), with no limit on how large each queue can become. If there is a sudden surge of incoming events for a Shopify store, the store's dedicated Mechanic queue could become delayed. This is an important difference between Mechanic and many other systems: in a high-traffic period, Mechanic will never refuse incoming events for a store; instead, it will process each one as soon as possible, by putting them into a run queue. The rate at which Mechanic processes work varies, depending on [concurrency](/core/runs/concurrency) and the [Shopify API rate limit](/core/shopify/api-rate-limit).


# Getting started

Install Mechanic and set up your first Shopify automation in minutes.

Mechanic is a Shopify automation and development platform with hundreds of ready-to-use tasks — and a full development environment for building your own.

{% stepper %}
{% step %}

## Install Mechanic

Install Mechanic from the [Shopify App Store](https://apps.shopify.com/mechanic). Once installed, you'll find it in your Shopify admin under **Apps > Mechanic**.
{% endstep %}

{% step %}

## Find a task in the task library

The fastest way to get started is to browse the [task library](https://tasks.mechanic.dev/) — a collection of hundreds of pre-built automations covering common Shopify workflows like auto-tagging, scheduled emails, inventory management, and more.

To install a task, use the [**Add task**](https://admin.shopify.com/apps/mechanic/tasks/add) page inside the app, open a task that fits your workflow, and click **Install this task**. Review the task's description, configure any options, and click **Save** to activate it. You can also browse [tasks.mechanic.dev](https://tasks.mechanic.dev/) to explore the library.

Many tasks have configurable options (like thresholds, tag names, or email templates) that let you customize behavior without writing any code.

{% hint style="info" %}
Can't find exactly what you need? You can [request a task](/resources/task-library/requesting) for the library — top-voted requests are regularly built.
{% endhint %}
{% endstep %}

{% step %}

## See it in action

Once you save a task, it's live — Mechanic will run it automatically when the matching event occurs. For example, a task that subscribes to `shopify/orders/create` will run every time a new order is placed.

To confirm it's working, open the [**Events**](/app/events) page in the Mechanic app. You'll see incoming events and the task runs they trigger, including any actions that were performed or errors that occurred.
{% endstep %}
{% endstepper %}

From here, you can:

* **Customize** — adjust the task's options or code to fit your workflow
* **Browse more tasks** — install additional tasks from the [task library](https://tasks.mechanic.dev/)
* **Go custom** — build your own task from scratch (see below)

## Go custom

If the task library doesn't cover your use case, Mechanic is a full development platform. Tasks are written in [Liquid](/platform/liquid/basics) with Mechanic-specific extensions for data manipulation, API access, and workflow composition. You get direct [GraphQL access](/core/shopify/read/graphql-in-liquid) to the Shopify Admin API, [HTTP actions](/core/actions/http) for calling any external service, and the ability to chain tasks together through [custom events](/core/actions/event) — all without managing servers or deployments.

* [I need something custom!](/custom) — an overview of the path to a custom task, whether you're a developer or need to find one
* [Your first custom task](/resources/tutorials/your-first-custom-task) — a step-by-step tutorial for building a task from scratch
* [Local task development with the Mechanic CLI](/resources/mechanic-cli) — edit, preview, diff, and publish tasks from your own editor

## Get help

* [Slack community](/resources/slack) — ask questions and share solutions with other Mechanic users
* [Hire a Mechanic Partner](/hire-a-developer) — find a developer to build or customize your task
* [Contact support](/support) — how to reach the Mechanic support team


# Hire a Mechanic Partner

Find a Shopify automation developer to build or customize Mechanic tasks — browse Mechanic Partners or bring your own developer.

Mechanic's automation tasks are written in Liquid, which is a template language used heavily in and around Shopify. This means that developers of all levels, with even a little Shopify development experience, can get started with Mechanic.

## When to hire a developer

Many merchants work with a developer to get exactly what they need. Here are some common scenarios:

* **Custom workflow** — you need an automation that isn't available in the [task library](https://tasks.mechanic.dev/)
* **Modifying a library task** — you've found a task that's close to what you need, but it requires changes beyond what its built-in options support
* **Third-party integration** — you want to connect Mechanic to an external service like an ERP, a custom app, or an API that isn't covered by the library
* **Ongoing maintenance** — you have a set of tasks that need regular updates as your business evolves

## Finding a developer

**To find a developer for hire, visit** [**partners.mechanic.dev**](https://partners.mechanic.dev/)**.** This is a growing list of established developers, both independent and agency, who can help you with your implementation.

If you're not sure where to start, try the [matchmaking service at partners.mechanic.dev/matchmaking](https://partners.mechanic.dev/matchmaking) — tell us what you need and we'll connect you with a developer who can help.

This is a super common path, and the Mechanic community is here to help. :)

If you already have a developer on your team, or have an existing connection to a developer, send them [this article](/custom) and see if they can help you!


# I need something custom!

Build custom Shopify automations with Mechanic using Liquid, GraphQL, and HTTP — or hire a developer to build one for you.

Mechanic is a development platform – in the hands of a developer, it can be used to accomplish almost anything in Shopify. While our [task library](/resources/task-library) covers many use cases, Mechanic's true strength is in letting developers solve merchant problems quickly, giving merchants easy configuration forms for managing the resulting tasks.

{% hint style="warning" %}
Mechanic support covers the platform and tasks from the [task library](/resources/task-library). Custom tasks (including AI-generated tasks) are not covered by support — for help with custom task logic, ask in the [Mechanic Slack community](/resources/slack) or [hire a developer](/hire-a-developer).
{% endhint %}

{% hint style="info" %}
Need something that you think others might need too? The Mechanic community accepts [task requests](/resources/task-library/requesting), and the top-voted requests are regularly selected for implementation.
{% endhint %}

## If you're using AI

We have tools for this! Mechanic offers an [MCP server](/resources/mcp) for task library and docs access, [Agent Skills](/resources/agent-skills) and a [Sidekick skill](https://github.com/lightward/mechanic-docs/blob/main/platform/sidekick.md) for task-writing patterns, and the [Mechanic GPT](https://github.com/lightward/mechanic-docs/blob/main/platform/chatgpt.md) for general help in ChatGPT. See [Using AI with Mechanic](/ai) for the full overview.

## If you need a developer…

Mechanic's automation tasks are written in Liquid, which is a template language used heavily in and around Shopify. This means that developers of all levels, with even a little Shopify development experience, can get started with Mechanic.

**To find a developer for hire, you can contact Mechanic Partners directly at** [**partners.mechanic.dev**](https://partners.mechanic.dev/)**.** This is a growing list of established developers, both independent and agency, who can help you with your implementation.

Lastly: if you already have a developer on your team, or have an existing connection to a developer, send them this article and see if they can help you!

## If you are a developer…

If you're familiar with Liquid and Shopify's Admin APIs, you're ready to build. Mechanic tasks are Liquid code with direct GraphQL access to Shopify, HTTP actions for external APIs, and custom events for composing multi-step workflows. Start by browsing the [task library](/resources/task-library) for patterns close to what you need, then modify or build from scratch using the code editor.

For task work that belongs in Git, code review, or an editor, use [Local task development with the Mechanic CLI](/resources/mechanic-cli). The CLI lets you create or pull task files, preview local changes with Mechanic, review diffs, and publish intentionally.

The [Core Concepts](/core/events) section covers the event-task-action model, and [Interacting with Shopify](/core/shopify) covers how tasks read and write Shopify data. The [Techniques](/techniques/writing-a-high-quality-task) section covers patterns that experienced Mechanic developers use.

Join Mechanic's [Slack workspace](/resources/slack) to exchange support with the community — the #general channel is a great place to start, and it's filled with people solving problems every day. :)


# Get help with a custom task

Get help with your custom Mechanic task — community Slack, developer hiring, and partner matchmaking.

{% hint style="info" %}
**What support is included with my Mechanic subscription?**

Our support team assists with **platform issues** and issues with tasks from the [Mechanic task library](https://tasks.mechanic.dev/). For these topics, contact us at <team@usemechanic.com>.

For custom tasks not included in our task library, our support service is limited to Mechanic platform support. See the options below for getting help with custom tasks.
{% endhint %}

## Getting help with custom tasks

### Slack Community

* **Community Support:** We encourage you to join our Slack community, where you can seek advice and share experiences with other Mechanic developers. Often, community members can offer insights or solutions based on their experiences.
* **Joining the Community:** You can join our Slack community through the following link: [Mechanic Slack Community](https://slack.mechanic.dev/).

### Hiring a Developer

* **Partner Directory:** We recommend hiring a developer if the issue requires more in-depth technical expertise. Our partner directory lists qualified developers familiar with Mechanic task development.
* **Finding a Developer:** Visit our partner directory at [partners.mechanic.dev](https://partners.mechanic.dev/) to find a developer who can customize or optimize your task. If you want to be matched with a suitable developer, use our [matchmaking service](https://partners.mechanic.dev/matchmaking).

<br>


# Using AI with Mechanic

Use AI tools to write, preview, diff, and publish Shopify automation tasks in Mechanic — CLI, MCP server, Agent Skills, Sidekick, and ChatGPT.

{% hint style="info" %}
[**Ask Mechanic**](/app/ask-mechanic) is Mechanic's built-in AI assistant, with access to Mechanic's docs and Shopify's GraphQL schema and API documentation. Use it right inside the app to ask questions, find tasks, and get help with task code — no setup required.

For developers writing tasks, Mechanic also offers dedicated AI tools:

* [**Mechanic CLI**](/resources/mechanic-cli) — preview, diff, and publish local task files safely
* [**Mechanic MCP Server**](/resources/mcp) — connect your AI coding tool to the task library and docs
* [**Mechanic Agent Skills**](/resources/agent-skills) — install task-writing expertise into your AI coding tool
* [**Mechanic Sidekick Skill**](https://github.com/lightward/mechanic-docs/blob/main/platform/sidekick.md) — write tasks with Shopify's built-in AI assistant
* [**Mechanic GPT**](https://github.com/lightward/mechanic-docs/blob/main/platform/chatgpt.md) — get help and write tasks in ChatGPT
  {% endhint %}

{% hint style="warning" %}
Mechanic support covers the platform and tasks from the [task library](/resources/task-library). AI-generated tasks are [custom tasks](/custom), and our support for custom tasks is limited to platform-level issues. For help with custom task logic, ask in the [Mechanic Slack community](/resources/slack) or hire a developer through [partners.mechanic.dev](https://partners.mechanic.dev/).
{% endhint %}

***

## AI tools for Mechanic

### Ask Mechanic (in-app)

**Ask Mechanic** is Mechanic's built-in AI assistant, available directly inside the app. It has access to Mechanic's full documentation and the [Shopify Dev MCP](https://github.com/lightward/mechanic-docs/blob/main/platform/shopify-dev-mcp.md) (Shopify's GraphQL schema and API documentation), so it can answer platform questions, help you find tasks, explain how features work, and assist with task code. Look for the **Ask Mechanic** button in the app to get started — no setup or external tools needed.

### For developers

The [**Mechanic CLI**](/resources/mechanic-cli) gives AI and agent workflows a safe local loop: pull task files, edit them in a repo, preview local changes, review diffs, and publish intentionally. Use it when an assistant is making changes that should be checked before they reach Mechanic.

The [**MCP server**](/resources/mcp) and [**Agent Skills**](/resources/agent-skills) are designed to work together. The MCP server gives your AI assistant access to Mechanic's task library and documentation at query time. Agent Skills install Mechanic's task-writing and CLI workflow patterns directly into your AI's context, so it knows how to structure subscriptions, handle preview mode, use GraphQL, and use the CLI to preview, diff, and publish local task files.

For the best results, use all three where they fit: the MCP server for research and reference, Agent Skills for writing code, and the CLI for previewing, diffing, and publishing local task files.

The [**Shopify Dev MCP Server**](https://github.com/lightward/mechanic-docs/blob/main/platform/shopify-dev-mcp.md) is also worth adding — it gives your AI assistant direct access to Shopify's GraphQL schema and query validation.

If you're working in Shopify Admin, the [**Sidekick skill**](https://github.com/lightward/mechanic-docs/blob/main/platform/sidekick.md) brings Mechanic task-writing expertise directly into Shopify's built-in AI assistant.

### For ChatGPT users

The [**Mechanic GPT**](https://github.com/lightward/mechanic-docs/blob/main/platform/chatgpt.md) has access to Mechanic's full documentation and can answer questions about the platform, explain concepts, and write or debug task code — all within ChatGPT. For agentic coding workflows, the MCP server and Agent Skills produce better results — but the GPT is a convenient option if ChatGPT is your primary tool.

***

## Tips for working with AI

These apply regardless of which AI tool you're using:

* **Start from the task library.** Search [tasks.mechanic.dev](https://tasks.mechanic.dev/) for an existing task that's close to what you need. Modifying a working task is faster and more reliable than starting from scratch.
* **Check subscriptions.** Make sure the task subscribes to the right event topics. A task that listens to `shopify/orders/create` won't fire on order updates.
* **Use native Shopify subscriptions by default.** Most Shopify-triggered tasks should subscribe to regular `shopify/...` topics. Use [custom Shopify webhooks](/platform/shopify/custom-webhooks) only when the task needs Shopify-side filtering, `include_fields`, metafield filtering or delivery, or metaobject events; those webhooks deliver onto `user/...` topics.
* **Prefer GraphQL.** Shopify REST is deprecated in Mechanic. Use the GraphQL Admin API for reading and writing Shopify data.
* **Verify the output format.** A complete Mechanic task is a JSON object with `name`, `script`, `subscriptions`, `subscriptions_template`, `options`, and other fields. If your AI produces only the Liquid code, it's missing the structure needed for import.
* **Watch for async vs sync confusion.** This is the most common AI mistake. `query | shopify` returns data immediately. `{% action "shopify" %}` queues a mutation that runs *after* the task code finishes. If the AI tries to use a mutation result in the same task run, the task will silently fail.
* **Check for preview mode.** Every event topic the task subscribes to needs an `{% if event.preview %}` block with mock data. If the task subscribes to 3 topics, there should be 3 preview blocks. Missing previews mean Mechanic can't verify the task will produce valid actions.
* **Webhook orders need `admin_graphql_api_id`.** When an order arrives via webhook (like `shopify/orders/create`), the order ID is REST-style. The task needs to use `order.admin_graphql_api_id` to get the full GraphQL ID for mutations. Using `order.id` directly will fail.


# Contact support

Get help with Mechanic — contact support, ask our AI assistant, join Slack, or hire a developer.

There are several ways to get help with Mechanic, whether you're troubleshooting a platform issue, looking for task development support, or need a developer for a custom project.

## Contact support

For **platform issues** and issues with tasks from the [Mechanic task library](https://tasks.mechanic.dev/), contact the Mechanic team directly:

**Email:** <team@usemechanic.com>

{% hint style="info" %}
Mechanic support covers the platform and [task library](/resources/task-library) tasks. For help with custom task logic (including AI-generated tasks), see the options below.
{% endhint %}

## Ask Mechanic

Mechanic includes a built-in AI assistant with access to Mechanic's full documentation and the [Shopify Dev MCP](https://github.com/lightward/mechanic-docs/blob/main/platform/shopify-dev-mcp.md) (Shopify's GraphQL schema and API documentation). It can answer platform questions, help you find tasks, explain how features work, and assist with task code. Look for the **Ask Mechanic** button inside the app.

## Slack community

The [Mechanic Slack workspace](/resources/slack) is where hundreds of developers and merchants compare implementation notes, collaborate on projects, and help each other troubleshoot. It's the best place to ask questions about custom tasks.

[**Join the Mechanic Slack workspace**](https://join.slack.com/t/usemechanic/shared_invite/zt-35thvnj5i-hlOy37OVU8f_QRCjcGPn~w)

## Hire a developer

If you need hands-on help building or customizing a task, you can hire a developer through the **Mechanic Partner Directory**:

* **Browse developers:** [partners.mechanic.dev](https://partners.mechanic.dev/)
* **Get matched with a developer:** [partners.mechanic.dev/matchmaking](https://partners.mechanic.dev/matchmaking)

Learn more: [Hire a Mechanic Partner](/hire-a-developer)

## AI tools for developers

Mechanic offers dedicated AI tools for developers writing tasks:

* [**Ask Mechanic**](/app/ask-mechanic) — built-in AI assistant with access to Mechanic docs and Shopify's GraphQL schema and API documentation
* [**Mechanic CLI**](/resources/mechanic-cli) — preview, diff, and publish local task files safely
* [**Mechanic MCP Server**](/resources/mcp) — connect your AI coding tool to the task library and docs
* [**Mechanic Agent Skills**](/resources/agent-skills) — install task-writing expertise into your AI coding tool
* [**Mechanic Sidekick Skill**](https://github.com/lightward/mechanic-docs/blob/main/platform/sidekick.md) — write tasks with Shopify's built-in AI assistant
* [**Mechanic GPT**](https://github.com/lightward/mechanic-docs/blob/main/platform/chatgpt.md) — get help and write tasks in ChatGPT

Learn more: [Using AI with Mechanic](/ai)

## System status

Check current platform status and subscribe to incident notifications:

[**status.mechanic.dev**](https://status.mechanic.dev)

## Related pages

* [🙋 I need something custom!](/custom) — starting points for custom development
* [🧑‍💻 Get help with a custom task](/custom-help) — options for custom task support
* [🤓 Hire a Mechanic Partner](/hire-a-developer) — finding a developer
* [🚀 Slack community](/resources/slack) — joining the community


# Task library

Browse and install hundreds of pre-built Mechanic tasks for common Shopify automation workflows.

Mechanic's **task library** is a compendium of e-commerce automation tasks and documentation, written by the Mechanic community and the Mechanic core team. [Hosted on GitHub](https://github.com/lightward/mechanic-tasks), everything is open-sourced under the highly permissive [MIT license](https://github.com/lightward/mechanic-tasks/blob/master/LICENSE), making all library tasks appropriate for re-use and modification.

**To browse the task library, visit** [**tasks.mechanic.dev**](https://tasks.mechanic.dev/)**.**

## Popular automations merchants install first

If you already know the automation you want Mechanic to handle, start with a task that's close:

* **Keep bundle and shared-SKU inventory aligned** — maintain bundle stock with [Maintain inventory for a product bundle](https://tasks.mechanic.dev/maintain-inventory-for-a-product-bundle), or sync variants with [Sync inventory for shared SKUs](https://tasks.mechanic.dev/sync-inventory-for-shared-skus).
* **Move, hide, publish, or sort products by rule** — move sold-out products with [Move out-of-stock products to the end of a collection](https://tasks.mechanic.dev/move-out-of-stock-products-to-the-end-of-a-collection), hide them with [Hide out-of-stock products](https://tasks.mechanic.dev/hide-out-of-stock-products), or publish new products automatically with [Auto-publish new products](https://tasks.mechanic.dev/auto-publish-new-products).
* **Fulfill or manage orders automatically** — fulfill digital or no-shipping items with [Auto-fulfill items that don't require shipping](https://tasks.mechanic.dev/auto-fulfill-items-that-dont-require-shipping), or close stale pending orders with [Cancel and close unpaid orders after x hours/days](https://tasks.mechanic.dev/cancel-and-close-unpaid-orders-after-two-days).
* **Tag wholesale, VIP, discount, or matching-product orders** — copy customer tags with [Auto-tag orders based on customer account tags](https://tasks.mechanic.dev/auto-tag-orders-based-on-customer-account-tags), copy discount codes with [Auto-tag orders by discount code](https://tasks.mechanic.dev/auto-tag-orders-by-discount-code), or tag orders that contain a matching product with [Auto-tag orders that contain a matching product](https://tasks.mechanic.dev/auto-tag-orders-that-contain-a-matching-product).
* **Email customers at the right moment** — email customers when an order is tagged with [Email customers when their order is tagged](https://tasks.mechanic.dev/email-customer-when-order-tagged), when tracking appears with [Email the customer when tracking numbers are added to their order](https://tasks.mechanic.dev/email-the-customer-when-tracking-numbers-are-added-to-their-order), or after they buy a product with [Email customers after purchasing a certain product](https://tasks.mechanic.dev/product-order-email).
* **Export CSV or PDF reports** — email order data with [Email a CSV export of orders](https://tasks.mechanic.dev/email-a-csv-export-of-orders), create PDF invoices with [Send a PDF invoice when an order is created](https://tasks.mechanic.dev/send-a-pdf-invoice-when-an-order-is-created), or build from a PDF example like [Generate a simple product catalog PDF](https://tasks.mechanic.dev/generate-a-simple-product-catalog-pdf).

These are starting points, not limits. Once installed, a task can be configured, copied, modified, or used as a pattern for something custom.

## Installing a task

1. Navigate to the **Add task** page in the Mechanic app to find a task that fits your needs. You can also browse [tasks.mechanic.dev](https://tasks.mechanic.dev/) to explore the library.
2. In the app, click **Install this task** to add the task to your Mechanic account
3. Configure any task options (settings fields) as needed
4. Click **Save** to activate the task

Once installed, a library task is your own copy. You can modify the code, change options, or adjust subscriptions to fit your specific requirements.

{% hint style="info" %}
When building a new task, it's often easier to start from an existing library task and modify it, rather than writing a task from scratch. Searching [tasks.mechanic.dev](https://tasks.mechanic.dev/) is a good place to start.
{% endhint %}

{% hint style="warning" %}
Mechanic support covers **unmodified** tasks from the task library. Once you modify a library task's code, it becomes a custom task, and support is limited to platform-level issues. For help with custom tasks, see [Get help with a custom task](/custom-help)
{% endhint %}

## What's in the library

The library includes hundreds of tasks covering common Shopify automation patterns, including:

* **Tagging** — auto-tag orders, customers, and products based on conditions
* **Notifications** — send emails, Slack messages, or other alerts on Shopify events
* **Inventory** — monitor stock levels, sync inventory across locations
* **Order management** — fulfill, cancel, or modify orders automatically
* **Product management** — publish, hide, or update products based on rules
* **Data feeds** — generate scheduled CSV or email reports
* **Integrations** — connect with external services like Google Sheets, Airtable, and more

## Contributing and requesting

**The Mechanic community can request new tasks – see** [**Requesting**](/resources/task-library/requesting)**.**

**The task library is open for contributions, by way of pull requests – see** [**Contributing**](/resources/task-library/contributing)**.**


# Contributing

Mechanic's [**task library**](/resources/task-library) is a central resource for the entire community, and is continually enriched through **contributions**, via pull requests [on GitHub](https://github.com/lightward/mechanic-tasks).

## Contributing your work to the task library

You've created a custom task, and you want to share it with the world! This brings us so much joy, and this is what the Mechanic project and this community are all about. If you get stuck along the way, please hop onto the [Slack workspace](https://join.slack.com/t/usemechanic/shared_invite/zt-cq84nrs7-ggYbYTbf~CrCjTg8nmHP2A), and we'll be glad to help.

{% hint style="info" %}
The task library is hosted in a Git repository on GitHub. You'll make your contribution via a [Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
{% endhint %}

We follow the same process any open-source project does when it comes to code management and code contributions. One bonus of contributing to the Mechanic task library is that once you learn the process here, you'll know how to contribute to open-source projects going forward.

### The process

* You'll [fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the task library [repository](https://github.com/lightward/mechanic-tasks).
  * Forking means taking a copy of our repository, so that you can make your changes and additions.
* Make your changes in your forked repository.
* Make a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests), which will trigger a review of your proposed changes and the merging of them into the main repository, making your task available to everyone using the app.

### Step-by-step instructions

1. You'll need a GitHub account, you can signup for one [here](https://github.com/join).
2. Visit the [task library repository](https://github.com/lightward/mechanic-tasks) and fork it as shown below. You'll make your changes to this copy of the repository.

   <img src="/files/-MSzUmDbqy80ROt2_3M0" alt="" data-size="original">
3. The task library is made up of the [tasks](https://github.com/lightward/mechanic-tasks/tree/master/tasks) and the supporting documentation. In these next few steps, you'll ensure you can build the [docs](https://github.com/lightward/mechanic-tasks/tree/master/docs), so that you can complete this step when you are ready to submit your contribution.

   1. Building the docs requires nodejs and npm. You can install them from here: <https://www.npmjs.com/get-npm>
   2. While in the project directory, run the following commands to build the docs:

   ```
   npm install   # install dependencies
   npm run build # compile docs
   npm run test  # apply sanity checks
   ```
4. Now that you can build the docs you are ready to [contribute](https://github.com/lightward/mechanic-tasks/blob/master/CONTRIBUTING.md)!
5. Your task documentation, options, subscriptions, code, are done in Mechanic. If you choose to use an external editor that's great, you still need to transfer it into Mechanic, so that you can export the task in the JSON format you need for the library. Importing/Exporting tasks from Mechanic is covered [here](/core/tasks/import-and-export).
6. If you're changing an existing task you [export](https://github.com/lightward/mechanic-tasks/blob/master/CONTRIBUTING.md) the JSON, and replace the contents of the `task/task_file_name.json` and then run the commands `npm run build` and `npm run test.`
7. If you are contributing a new task, you'll [export](/core/tasks/import-and-export) the JSON from Mechanic, and save the JSON file in the `tasks/directory` of your forked repository, named with an appropriate handle for the task. (For example, a task named "Hide out-of-stock products" should have its JSON export stored in `"tasks/hide-out-of-stock-products.json"`.) And, then you'll execute the commands:

   `npm run build` and `npm run test`.
8. If all goes well with the build, you'll see your task listed in the automatically created documentation in `docs/README.md`
9. You're now ready to make your pull request! Head over to <https://github.com/lightward/mechanic-tasks/pulls> and click New pull request, you should see the changes you committed to your fork, and you'll proceed with filling out the pull request form.
10. After you submit your first pull request, you will be required to read and accept [our CLA](https://github.com/lightward/mechanic-tasks/blob/master/CLA.md). The[ CLA assistant](https://github.com/marketplace/actions/cla-assistant-lite) will leave a comment, giving you a statement of agreement that you must paste into a comment of your own.
11. This process could sound confusing if you haven't done it before, but once you've done it once, it's simple and it is also pretty exciting to go through the process. The other bonus is, you'll be ready to submit a pull request to any open-source software project in the future. If you need help please out to us in the [community Slack workspace](https://slack.mechanic.dev/).


# Requesting

The Mechanic community maintains a board of task requests, where anyone can submit their idea for an addition to the Mechanic task library.

{% embed url="<https://mechanic.canny.io/task-requests>" %}

### What kind of task requests are accepted?

We accept task requests in two categories:

* Tasks that are broadly useful off-the-shelf for non-technical users
* Tasks that are useful foundations for developers, for further modification or inspiration

{% hint style="info" %}
If you need a task that's specifically tailored to a unique situation, or if you have a tight timeline, the task requests board is likely not a good fit. Instead, [work with a developer one-on-one](/custom) to create something that suits your needs specifically.
{% endhint %}

### Who implements these task requests?

The Mechanic staff commission these from developers in the Mechanic community. If you're a developer interested in receiving this kind of work, get in touch at <team@usemechanic.com>.

### Where can I file a task request?

Head to <https://mechanic.canny.io/task-requests>. :)


# Custom webhook imports

Import files for library tasks that use custom Shopify webhooks

### [Monitor for variants with zero price](https://tasks.mechanic.dev/monitor-for-variants-with-zero-price)

This task requires importing this custom Shopify webhook or creating it manually.

{% code title="variants-with-zero-price-custom-webhook.json" %}

```json
{
  "name": "Variants with zero price",
  "shopify_topic": "shopify/products/update",
  "event_topic": "user/product_update/variants_zero_price",
  "filter": "variants.price:\"0.00\" AND status:active AND published_at:*",
  "include_fields": [
    "id",
    "updated_at",
    "title",
    "status",
    "published_at",
    "variants.id",
    "variants.updated_at",
    "variants.title",
    "variants.sku",
    "variants.price"
  ],
  "metafield_namespaces": [],
  "metafields": []
}
```

{% endcode %}


# Local task development

Edit Mechanic tasks locally, preview and diff changes, and publish them safely with the Mechanic CLI.

Use the Mechanic CLI when you want to keep a shop's tasks in local files, edit them in your own tools, preview changes with Mechanic, and publish them back when they are ready.

This is the recommended path for task work that belongs in Git, needs review, or benefits from an editor, an AI coding tool, or a repeatable deployment process.

{% hint style="info" %}
The CLI uses Mechanic's task sync API, authenticated with an API token from your shop. If you are building custom scripts, see [API tokens and task sync API](/resources/mechanic-cli/mechanic-task-sync-api). For most users, start with the CLI.
{% endhint %}

## How it fits together

Each CLI project is connected to one Mechanic shop. The project contains:

* `mechanic.json`, which identifies the shop and API host
* `.mechanic/links.json`, which remembers which local files match which tasks in Mechanic
* `tasks/*.json`, which are the canonical task files

Mechanic task JSON is the portable format. It is the same general shape used by [Import and export](/core/tasks/import-and-export). Because editing long Liquid and Markdown strings inside JSON is awkward, the CLI can also split one task into a helper folder with separate files for Liquid, docs, subscriptions, and JavaScript.

In short:

* JSON files are the source of truth for syncing.
* Helper folders are the comfortable editing format.
* Bundle helper folders back into JSON before publishing.

## Set up a local task project

{% stepper %}
{% step %}

### Install the CLI

The Mechanic CLI requires Node.js 22 or higher.

```bash
npm install -g @lightward/mechanic-cli
```

To check your installed CLI version against the latest published version:

```bash
mechanic version
```

{% endstep %}

{% step %}

### Create an API token

In the Mechanic app, open **Settings > API tokens** and create a token for this device, repository, or automation.

Copy the token immediately. Mechanic only shows it once.
{% endstep %}

{% step %}

### Initialize the project

Create a folder for the shop's task files, then initialize it:

```bash
mkdir example-mechanic-tasks
cd example-mechanic-tasks
mechanic init --shop example.myshopify.com
```

When prompted, paste the API token from Mechanic. The CLI stores it outside the project folder.
{% endstep %}

{% step %}

### Choose how to start

After initializing the project, choose one path.

**Bring one existing task into local files:**

```bash
mechanic tasks list --verbose
mechanic tasks pull <remote-task-id>
```

This creates one local JSON file in `tasks/` and records the link to the matching task in Mechanic. Use this when you want to start with a specific task. The task list also shows each task's configured Shopify API version, which is useful when deciding which tasks may need an API version review.

**Bootstrap the repo with every task in the shop:**

```bash
mechanic tasks pull
```

Running `tasks pull` without a task selector pulls every task for the shop. Use this when you are intentionally setting up a Git repo for all of a shop's tasks.

**Start from a new local task:**

```bash
mechanic tasks new order-tagger
```

This creates a starter task in `tasks/order-tagger.json` and `tasks/order-tagger/`. Nothing is created in Mechanic until you publish.

For most commands, the simplest selector is the local task slug, like `order-tagger`. The CLI also accepts a task JSON file, helper folder, or linked remote task ID. If a slug matches more than one local file, the CLI will ask for the full `tasks/<name>.json` path.

Task names and local file names are related, but they are not the same thing. When the CLI first pulls or creates a task, it uses the task name to choose a readable local slug like `order-tagger`, which becomes `tasks/order-tagger.json`.

If you rename the task in Mechanic, the next pull keeps the existing local file name and updates the `name` field inside the JSON. If you rename the local JSON file or helper folder by hand, the task may appear unlinked. Prefer keeping local filenames stable, and run `tasks status` after renaming anything.
{% endstep %}
{% endstepper %}

## Edit, preview, and publish one task

{% stepper %}
{% step %}

### Choose a task

Pick a task file from `tasks/`, or run:

```bash
mechanic tasks list --verbose
```

`--verbose` shows remote task IDs, linked local files, configured Shopify API versions, and sync details.

To start from scratch instead, create a new blank local task:

```bash
mechanic tasks new order-tagger
```

This creates a blank starter task in both `tasks/order-tagger.json` and `tasks/order-tagger/`. You can use it any time in an initialized CLI project; it does not require a fresh repository. Nothing is created in Mechanic until you publish, and the command will not overwrite existing local task files unless you pass `--force`.
{% endstep %}

{% step %}

### Unbundle it for editing

If you pulled an existing task and only have the JSON file, unbundle it before editing Liquid or docs:

```bash
mechanic tasks unbundle order-tagger
```

This creates a helper folder:

```
tasks/
  order-tagger.json
  order-tagger/
    task.json
    script.liquid
    docs.md
    subscriptions.liquid
```

Tasks created with `tasks new` already have this helper folder, so you can edit them without running `tasks unbundle` first.
{% endstep %}

{% step %}

### Edit and bundle

Edit the helper files, then bundle them back into the JSON file:

```bash
mechanic tasks bundle order-tagger
```

{% endstep %}

{% step %}

### Check status

Check where things stand:

```bash
mechanic tasks status
```

This checks local files and task links, so you can see whether a task is ready or needs bundling. For smaller projects, it also checks the current linked tasks in Mechanic. For larger projects, repo-wide status stays local to avoid hundreds of API calls; pass a task selector when you want to check one task's remote state:

```bash
mechanic tasks status order-tagger
```

If a helper folder has changes that have not been bundled, the CLI will stop before publishing and tell you what to run. Use `--local` when you explicitly want to skip remote checks.
{% endstep %}

{% step %}

### Preview locally

`tasks preview` sends the local task content to Mechanic's preview engine without saving it. It reports validation errors, previewed actions, and Shopify permissions detected by the previewed paths. Missing Shopify permissions are approved in the Mechanic app after publishing or enabling the task.

```bash
mechanic tasks preview order-tagger
```

Use `--verbose` if you want the terminal to show event, task run, and action run details without switching to JSON output:

```bash
mechanic tasks preview order-tagger --verbose
```

`tasks preview --stdin` reads task JSON from stdin instead of local files. See [AI and agent workflows](#ai-and-agent-workflows) for editor and automation use.
{% endstep %}

{% step %}

### Review the diff

`tasks diff` compares the version currently saved in Mechanic with your local file. In diff output, `-` is Mechanic and `+` is your local file.

If only your local file changed, continue to the publish preflight. If Mechanic changed and your local file has not, pull normally to bring the Mechanic version into your repo. If both changed, reconcile the changes first when you can.

When you intentionally need one version to replace the other, the command tells the CLI which version to keep:

```bash
mechanic tasks pull <remote-task-id> --force
mechanic tasks publish order-tagger --force
```

`pull --force` keeps the current Mechanic version. `publish --force` keeps your local file.

```bash
mechanic tasks diff order-tagger
```

{% endstep %}

{% step %}

### Check the publish plan

`tasks publish --dry-run` is a publish preflight. It checks whether publishing would be safe, including helper-folder drift and remote conflicts, then prints what would create, update, stay unchanged, or stop before writing anything to Mechanic.

```bash
mechanic tasks publish order-tagger --dry-run
```

{% endstep %}

{% step %}

### Publish intentionally

`tasks publish` writes the selected local task to Mechanic. If the task is already enabled, the newly published version is live for future runs immediately. Publishing does not turn enabled tasks off or disabled tasks on.

New tasks are created disabled, so you can review and enable them in the app when they are ready.

```bash
mechanic tasks publish order-tagger
```

After publishing, open the task in Mechanic to review the saved version:

```bash
mechanic tasks open order-tagger
```

In Mechanic, confirm the saved version, approve any Shopify permissions if prompted, and enable the task if it was newly created. Existing enabled tasks keep running with the published version.

{% hint style="warning" %}
Use `publish --force` only when you intentionally want the local file to replace changes made in Mechanic or another checkout. The default workflow is designed to stop before stale local files overwrite newer remote changes.
{% endhint %}
{% endstep %}
{% endstepper %}

## Before publishing

For a normal task change, this is the confidence loop:

```bash
mechanic tasks status order-tagger
mechanic tasks preview order-tagger
mechanic tasks diff order-tagger
mechanic tasks publish order-tagger --dry-run
```

Use `tasks open` to jump from a local task file, helper folder, slug, or remote task ID to the task in Mechanic:

```bash
mechanic tasks open order-tagger
```

## Monitor queue lag

`shop status` shows the shop's current Mechanic queue and backlog: running runs, waiting runs, queue lag, and the tasks, actions, or event topics that are contributing to the wait.

Use it when you want to monitor whether a shop is falling behind:

```bash
mechanic shop status
```

For external monitoring tools, use JSON output from a scheduled job and alert on waiting runs or queue lag:

```bash
mechanic shop status --json
```

This can be used with a service like Cronitor, UptimeRobot, Better Stack, or your own monitoring system. It is not required for publishing task changes. For integrations that prefer direct HTTP requests, use [`GET /v1/shop/status`](/resources/mechanic-cli/mechanic-task-sync-api) with a Mechanic API token.

## Review Shopify API deprecations

`shop deprecations` shows unresolved Shopify API deprecations reported by tasks in the configured shop:

```bash
mechanic shop deprecations
```

Use it when you want to find tasks that are still calling deprecated Shopify API surfaces. The output includes the task's configured Shopify API version, the API version seen in the deprecated request, the request path, and the last time Mechanic saw it.

To focus on one linked task, pass the same task selector you would use for preview, diff, or publish:

```bash
mechanic shop deprecations order-tagger
```

Use `--json` when a script, agent, or dashboard needs structured output.

## Manage shop globals and secrets

The CLI can also manage [shop globals and secrets](/platform/globals-and-secrets).

Globals are visible JSON values. They can be pulled into a repo-safe file:

```bash
mechanic globals pull
```

This writes `mechanic.globals.json` by default. The file may be committed to Git, reviewed, and shared with the task repo.

If the output file already exists and differs, `globals pull` stops before overwriting it. Use `--force` only when you mean to replace the local file:

```bash
mechanic globals pull --force
```

To push globals from a file:

```bash
mechanic globals push --dry-run
mechanic globals push --force
```

`globals push` validates the file before writing anything to Mechanic. It updates listed keys and does not delete remote globals that are missing from the file. If an existing remote global has a different value, the command stops unless you pass `--force`.

For one-off global changes:

```bash
mechanic globals list
mechanic globals set warehouse_id --json '"main"'
mechanic globals set shipping_rules --json '{"regions":["CA","US"]}'
mechanic globals delete warehouse_id --force
```

Secrets are write-only. The CLI lists secret metadata, but never prints secret values and never creates a secrets file.

```bash
mechanic secrets list
mechanic secrets set api_token
mechanic secrets set api_token --value-env API_TOKEN
printf %s "$API_TOKEN" | mechanic secrets set api_token --from-stdin --force
mechanic secrets delete api_token --force
```

`secrets set --from-stdin` preserves exact stdin content, including leading or trailing whitespace and trailing newlines. `secrets set --force` is required when replacing an existing secret. Deleting or replacing a secret can break tasks until they are updated.

## Git and GitHub Actions

You can commit `mechanic.json`, `.mechanic/links.json`, and `tasks/` to Git. This gives your task changes history, review, and rollback.

Once the local workflow is working, the CLI can generate optional GitHub Actions:

```bash
mechanic github init
```

Local CLI usage works without GitHub Actions. If you add them, the generated workflows can validate task files on pull requests, run a manual dry-run/deploy, and open a sync-back PR for changes pulled from Mechanic. After a deploy, the workflow also opens or updates a sync-state PR so `.mechanic/links.json` and task hashes stay current.

`mechanic github init` creates:

```
.github/workflows/mechanic-validate.yml
.github/workflows/mechanic-deploy.yml
.github/workflows/mechanic-sync-from-app.yml
```

Pull request validation does not need a Mechanic token. Deploy and sync workflows do. Store that token as a GitHub secret named `MECHANIC_API_TOKEN`.

{% hint style="warning" %}
Only add deploy or sync workflows to repositories maintained by people you trust. Those workflows use `MECHANIC_API_TOKEN`, which can read, preview, and publish tasks for the shop, and can manage globals and secrets through the v1 API.
{% endhint %}

The sync-from-app workflow is update-only in this version. It does not delete local files for tasks deleted in Mechanic. Generated GitHub workflows do not sync `mechanic.globals.json` automatically; add that explicitly if your repo needs it.

## AI and agent workflows

The CLI pairs well with AI coding tools:

* Use [Mechanic MCP Server](/resources/mcp) so your assistant can search Mechanic docs and task library examples.
* Use [Mechanic Agent Skills](/resources/agent-skills) so your assistant knows how to write Mechanic tasks.
* Use the CLI to preview, diff, dry-run, and publish the task files the assistant edits.

For agent-friendly output, many commands support machine-readable or automation-friendly flows, such as:

```bash
mechanic tasks preview order-tagger --json
```

Editors and other tools can preview in-memory task content without writing it to disk first. `tasks preview --stdin` reads task JSON from stdin. Pass a task selector to preview the piped content in that linked task's context, or omit the selector to preview it as a new unlinked task:

```bash
cat build/order-tagger.json | mechanic tasks preview order-tagger --stdin --json
```

`--stdin` treats the piped content as the source of truth, so it skips the stale helper directory check. Preview exits `0` when the preview passes, `1` when sample runs fail, and `2` when the task is invalid.

When a `--json` command fails before producing its normal output (auth problems, network failures, rate limits), it prints a JSON error envelope to stdout instead, with exit codes unchanged:

```json
{ "error": { "message": "Mechanic API rate limit exceeded.", "status": 429, "retry_after_seconds": 7 } }
```

With `--json`, stdout is always a single JSON document: the command's normal output, or this error envelope. Previews are [rate limited](/resources/mechanic-cli/mechanic-task-sync-api#rate-limits-and-errors) per token; tools that preview on every edit should debounce, keep one preview in flight, and wait `retry_after_seconds` before retrying.

## Related

* [API tokens and task sync API](/resources/mechanic-cli/mechanic-task-sync-api)
* [Import and export](/core/tasks/import-and-export)
* [Previews](/core/tasks/previews)
* [Mechanic MCP Server](/resources/mcp)
* [Mechanic Agent Skills](/resources/agent-skills)


# API tokens and task sync API

Use Mechanic API tokens with the v1 API for trusted scripts, agents, CI, and the Mechanic CLI.

Mechanic API tokens authenticate v1 API workflows for one shop. They are used by the [Mechanic CLI](/resources/mechanic-cli), and may also be used by trusted scripts, agents, and CI jobs that need direct HTTP access to task sync, shop status, Shopify API deprecation visibility, and [globals and secrets](/platform/globals-and-secrets).

{% hint style="info" %}
The CLI is the recommended client. It wraps this API with local task validation, content hashes, idempotency keys, preview commands, diff output, and safer publish flows. Direct HTTP use is for CLI-compatible automation, not a broad public API platform.
{% endhint %}

## API tokens

Create API tokens in the Mechanic app at **Settings > API tokens**.

Tokens are:

* shop-scoped
* shown once at creation
* revocable from Mechanic settings
* visible later only by name, creator, creation time, last-used time, and status
* accepted as `Authorization: Bearer <token>`
* usable as `MECHANIC_API_TOKEN` for the CLI and CI jobs

Treat tokens like passwords. A token can read task sync payloads, preview and publish tasks, read and write globals, and set, replace, or delete secrets for its shop until revoked.

Use one token per device, repository, or automation. Each shop can have up to five active API tokens. If a token is leaked, revoke it and create a replacement.

{% hint style="warning" %}
API tokens are not granularly scoped in this version. Do not share a token with untrusted repositories, third-party scripts, or agents you would not trust to read and publish tasks or manage globals and secrets for the shop.
{% endhint %}

## Base URL and authentication

Production API base URL:

```
https://api.mechanic.dev
```

Authenticate with a bearer token:

```bash
curl https://api.mechanic.dev/v1/auth/verify \
  -H "Authorization: Bearer $MECHANIC_API_TOKEN"
```

Without an expected shop, this only confirms that the token is valid. To verify that a token is valid for the shop your automation intends to use, include the shop domain:

```bash
curl https://api.mechanic.dev/v1/auth/verify \
  -H "Authorization: Bearer $MECHANIC_API_TOKEN" \
  -H "X-Mechanic-Shop-Domain: example.myshopify.com"
```

For local CLI use, store the token outside the project with:

```bash
mechanic auth login
```

For CI jobs, store the token as a secret named `MECHANIC_API_TOKEN` and expose it to the job environment. Avoid typing raw tokens into shared shell commands, where history or process listings may expose them.

## Endpoints

This API is intentionally narrow. It covers task sync, task preview, shop queue status, Shopify API deprecation visibility, and shop globals and secrets for the authenticated shop. Use direct HTTP only for trusted automation that can safely store a shop API token.

| Method   | Path                    | Purpose                                                                                                         |
| -------- | ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| `GET`    | `/v1/auth/verify`       | Verify the API token; include an expected shop so mismatched tokens are rejected without revealing another shop |
| `GET`    | `/v1/shop/status`       | Read current queue, backlog, and lag status                                                                     |
| `GET`    | `/v1/shop/deprecations` | Read unresolved Shopify API deprecations reported by the shop's tasks                                           |
| `GET`    | `/v1/tasks`             | List tasks available to sync                                                                                    |
| `GET`    | `/v1/tasks/:id`         | Read one task for sync                                                                                          |
| `POST`   | `/v1/tasks/preview`     | Preview local task content without saving                                                                       |
| `POST`   | `/v1/tasks/:id/preview` | Preview local draft content against an existing task, or preview the current saved task, without saving         |
| `POST`   | `/v1/tasks`             | Create a task from sync payload                                                                                 |
| `PUT`    | `/v1/tasks/:id`         | Update a task from sync payload                                                                                 |
| `GET`    | `/v1/globals`           | List shop globals, including values                                                                             |
| `PUT`    | `/v1/globals/:key`      | Create or replace one shop global                                                                               |
| `DELETE` | `/v1/globals/:key`      | Delete one shop global                                                                                          |
| `GET`    | `/v1/secrets`           | List shop secret metadata only                                                                                  |
| `PUT`    | `/v1/secrets/:key`      | Create or replace one shop secret                                                                               |
| `DELETE` | `/v1/secrets/:key`      | Delete one shop secret                                                                                          |

{% hint style="info" %}
There is no `tasks new` API endpoint. `mechanic tasks new` is a local CLI command that creates a starter task file and helper folder in an initialized project. When that new local task is published, the CLI uses `POST /v1/tasks`, and Mechanic creates the remote task disabled.

`POST /v1/tasks` always creates a new task. It does not update an existing task by matching the task name or local slug. For direct HTTP creates, send an `Idempotency-Key` header with a UUID generated for the task you intend to create. Mechanic uses that UUID as the created task ID. Retrying with the same UUID returns the existing task instead of creating a duplicate, so use a different UUID for each intended task.
{% endhint %}

For most local task work, prefer the CLI commands that wrap these endpoints:

```bash
mechanic tasks preview order-tagger --json
mechanic tasks publish order-tagger --dry-run
mechanic tasks publish order-tagger
mechanic shop status
mechanic shop deprecations
```

Use `mechanic shop status --json`, or `GET /v1/shop/status` directly, when you want an external monitor to alert on queue lag or waiting runs for the shop.

Use `mechanic shop deprecations --json` when a trusted script, agent, or dashboard needs structured deprecation data. Use `GET /v1/shop/deprecations` directly only when the automation needs to make HTTP requests itself. Add `task_id` to focus on one task:

```bash
curl "https://api.mechanic.dev/v1/shop/deprecations?task_id=<task-id>" \
  -H "Authorization: Bearer $MECHANIC_API_TOKEN"
```

The response is capped and includes `total_count`, `limit`, and `truncated`. Deprecation entries include task identity, task Shopify API version, request API version, request method/path, occurrence count, and first/last occurrence times. They do not include request bodies, request headers, response headers, tokens, or other raw payload data.

`GET /v1/tasks` and `GET /v1/tasks/:id` also include `shopify_api_version` in task sync envelopes, so scripts can see the configured API version without fetching or parsing the task JSON separately.

## Globals and secrets

Globals and secrets are shop-level configuration values. See [Globals and secrets](/platform/globals-and-secrets) for task-author usage and safety details.

Globals store visible JSON values. `GET /v1/globals` returns those values:

```json
{
  "globals": [
    {
      "key": "warehouse_id",
      "value": "main",
      "created_at": "2026-06-10T12:00:00Z",
      "updated_at": "2026-06-10T12:00:00Z"
    }
  ]
}
```

Set or replace a global with:

```bash
curl https://api.mechanic.dev/v1/globals/warehouse_id \
  -X PUT \
  -H "Authorization: Bearer $MECHANIC_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"global":{"value":"main"}}'
```

Secrets store write-only string values. Secret responses never include the saved value. `GET /v1/secrets` returns metadata only:

```json
{
  "secrets": [
    {
      "key": "api_token",
      "value_present": true,
      "approximate_task_reference_count": 2,
      "created_at": "2026-06-10T12:00:00Z",
      "updated_at": "2026-06-10T12:00:00Z"
    }
  ]
}
```

Set or replace a secret by sending this JSON shape to `PUT /v1/secrets/:key`:

```json
{
  "secret": {
    "value": "<secret-value>"
  }
}
```

Avoid putting raw secret values in shell history. For most workflows, prefer the CLI's `mechanic secrets set --value-env` or `mechanic secrets set --from-stdin` commands.

## Preview and publish behavior

Preview requests do not create, update, save, enable, or run a task. They ask Mechanic to evaluate local or remote task content through preview mode and return the result.

Use `POST /v1/tasks/preview` for local task content that is not linked to an existing Mechanic task. Use `POST /v1/tasks/:id/preview` when you want to preview draft local content in the context of an existing task. If no task body is sent to `/v1/tasks/:id/preview`, Mechanic previews the current saved task.

Publishing writes task content to Mechanic. It does not enable or disable an existing task. New tasks created through task sync are created disabled. Review and enable them in the app when they are ready.

Updates require remote-change protection. Send `previous_content_hash` with `PUT /v1/tasks/:id` so Mechanic can reject stale updates instead of overwriting newer changes. This must be the `content_hash` from the task version your client last read from Mechanic, not a hash of the outgoing local payload. Use `force` only as an intentional bypass when your local payload should replace the current Mechanic task.

## Rate limits and errors

The task sync API has its own rate limits, separate from Shopify Admin API rate limits. If Mechanic returns `429 Too Many Requests`, respect the `Retry-After` response and retry later.

Current task sync limits:

* public API requests: 120/min per IP, checked before token authentication
* authenticated API token requests: 300/min per token
* task writes: 120/min per token
* shop status requests: 60/min per token
* task previews: 30/min per token
* preview concurrency: at most 2 running previews per token, and at most 4 running previews per shop through the task sync API

Avoid tight preview loops in CI or agents.

Common responses:

| Status | Meaning                                                                                               |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, or revoked API token                                                                |
| `403`  | Token is not allowed to access this shop or API surface                                               |
| `400`  | Invalid request, validation error, or missing update guard such as `previous_content_hash` or `force` |
| `404`  | Task not found for this shop                                                                          |
| `409`  | Remote task changed since the last known content hash                                                 |
| `429`  | Rate limit reached; retry after the indicated delay                                                   |

## GitHub Actions

If you use generated GitHub Actions, store the token as a repository or organization secret named `MECHANIC_API_TOKEN`.

Pull request validation does not need a Mechanic token. Deploy and sync workflows do.

{% hint style="warning" %}
Use deploy and sync workflows only in trusted repositories. Consider GitHub environment protection, branch restrictions, and reviewer approval before workflows can publish to Mechanic.
{% endhint %}

## What is not included yet

This is not a general-purpose Mechanic API reference. This version does not document SDKs, token scopes, task deletion, task enabling/disabling, event APIs, run APIs, webhook management APIs, fleet management, or formal public API versioning beyond the documented v1 surface.

Start with [Local task development with the Mechanic CLI](/resources/mechanic-cli) unless you specifically need direct HTTP automation.


# Mechanic MCP Server

Connect your AI coding tool to Mechanic's task library and documentation.

Connect your AI assistant to Mechanic's task library and documentation. The Mechanic Model Context Protocol (MCP) server enables your AI assistant to search tasks, explore documentation, and fetch task code.

***

## How it works

Your AI assistant uses the MCP server to read and interact with Mechanic's resources:

1. Ask your AI assistant to help with Mechanic task development or automation questions.
2. The assistant searches Mechanic's task library and documentation based on your prompt.
3. The MCP server provides access to 350+ pre-built tasks and comprehensive Mechanic documentation, so your assistant can provide accurate code, solutions, and guidance based on current best practices.

***

## Requirements

Before you set up the Mechanic MCP server, make sure you have:

* **Node.js 18 or higher** installed on your system.
* An **AI development tool** that supports MCP, such as Claude Desktop, Claude Code, Cursor, Codex CLI, or Gemini CLI.

***

## What you can ask your AI assistant

After you set up the MCP server, you can ask your AI assistant questions like:

* "Find tasks that auto-tag orders"
* "How do I subscribe to Shopify product creation events?"
* "Show me the code for the 'Auto-tag customers by order tier' task"
* "What tasks use the bulk operations API?"
* "How do I use the cache action in Mechanic?"
* "Find similar tasks to 'email customer when order is fulfilled'"
* "Write a task that sends an email when inventory gets low"

Your AI assistant will use the MCP server to search Mechanic's task library and documentation when providing responses.

***

## Available resources

The MCP server provides access to:

* **350+ pre-built automation tasks** from [tasks.mechanic.dev](https://tasks.mechanic.dev)
* **Complete Mechanic documentation** from [learn.mechanic.dev](https://learn.mechanic.dev)
* Task subscriptions, options, and full Liquid code
* Documentation on Liquid templating, actions, events, and integrations

***

## Set up the server

The server runs locally in your development environment and doesn't require authentication.

### Step 1: Configure your AI development tool

Add configuration code that tells your AI tool how to connect to and use the Mechanic MCP server. This configuration enables your AI assistant to automatically access Mechanic's task library and documentation when you ask questions.

#### Claude Desktop and Claude Code

**For Claude Desktop:**

1. Open the app and access your configuration file through settings
2. Add the JSON configuration below to your MCP servers section
3. Save and restart Claude Desktop

**For Claude Code:**

1. Run this command in your terminal:

   ```bash
   claude mcp add --scope user --transport stdio mechanic-mcp -- npx -y @lightward/mechanic-mcp@latest
   ```
2. Restart Claude Code to load the server

**Manual JSON configuration (both):**

{% tabs %}
{% tab title="Configuration" %}

```json
{
  "mcpServers": {
    "mechanic-mcp": {
      "command": "npx",
      "args": ["-y", "@lightward/mechanic-mcp@latest"]
    }
  }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For more information, read the [Claude Desktop MCP guide](https://modelcontextprotocol.io/quickstart/user).
{% endhint %}

#### Cursor

1. Open Cursor and go to **Cursor** > **Settings** > **Cursor Settings** > **Tools and integrations** > **New MCP server**.
2. Add this configuration to your MCP servers:

{% tabs %}
{% tab title="Configuration" %}

```json
{
  "mcpServers": {
    "mechanic-mcp": {
      "command": "npx",
      "args": ["-y", "@lightward/mechanic-mcp@latest"]
    }
  }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For more information, see the [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol).
{% endhint %}

1. Save your configuration and restart Cursor.

#### Codex CLI

1. Add this configuration to your `~/.codex/config.toml` file:

{% tabs %}
{% tab title="Configuration" %}

```toml
[mcp_servers.mechanic-mcp]
command = "npx"
args = ["-y", "@lightward/mechanic-mcp@latest"]
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Codex uses TOML format with `mcp_servers` (snake\_case) instead of JSON with `mcpServers` (camelCase).
{% endhint %}

1. Restart Codex to load the new MCP server configuration.

#### Gemini CLI

1. Add this configuration using the Gemini CLI:

{% tabs %}
{% tab title="Configuration" %}

```json
{
  "mcpServers": {
    "mechanic-mcp": {
      "command": "npx",
      "args": ["-y", "@lightward/mechanic-mcp@latest"]
    }
  }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
By default, this adds the server to your project configuration. To make it available across all projects, add the `--scope user` flag. For more information, see the [Gemini CLI MCP documentation](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html).
{% endhint %}

1. Restart Gemini CLI to load the new MCP server configuration.

***

## Available tools

The Mechanic MCP server provides the following tools:

### `search_tasks`

Search across Mechanic's task library to find tasks matching your query. Returns task titles, descriptions, tags, subscriptions, and public URLs.

Best for discovering existing automation solutions before building custom tasks. Results include subscription event topics, task options, and tags for filtering.

**Parameters:**

* `query` (required): Search terms to match against task titles, descriptions, and content
* `limit` (optional): Maximum number of results to return (default: 10, max: 50)
* `offset` (optional): Number of results to skip for pagination (default: 0)
* `tags` (optional): Filter by task tags
* `subscriptions` (optional): Filter by event subscription topics
* `fuzzy` (optional): Enable fuzzy matching for typo tolerance
* `fuzzyMaxEdits` (optional): Maximum edit distance for fuzzy matching (max: 2)

***

### `search_docs`

Search across all Mechanic documentation to find relevant pages matching your query. Returns documentation titles, paths, and public URLs.

Best for learning about Mechanic concepts, Liquid templating, actions, and platform features. Returns quick results from across the entire documentation site.

**Parameters:**

* `query` (required): Search terms to match against documentation content
* `limit` (optional): Maximum number of results to return (default: 10, max: 50)
* `offset` (optional): Number of results to skip for pagination (default: 0)
* `fuzzy` (optional): Enable fuzzy matching for typo tolerance

***

### `get_task`

Retrieve complete task details including subscriptions, Liquid code, options, and JavaScript blocks. Use this to see the full implementation of a specific task.

**Parameters:**

* `id` (required): Task handle or ID (e.g., "auto-tag-customers-by-sales-channel" or "task:auto-tag-customers-by-sales-channel")

**Returns:**

* Task metadata (name, tags, URL)
* Event subscriptions and subscription template
* Complete Liquid code
* Task options with defaults
* Online store JavaScript (if present)
* Order status JavaScript (if present)

{% hint style="info" %}
When sharing task code, return only the relevant parts (subscriptions and Liquid code), not the full JSON export. Always include the public task URL.
{% endhint %}

***

### `get_doc`

Retrieve the full content of a documentation page. Provides complete documentation context without chunking.

**Parameters:**

* `id` (required): Documentation ID from search results (e.g., "doc:core/tasks/code/action-objects.md")

**Returns:**

* Full markdown content
* Documentation title and path
* Public documentation URL

{% hint style="info" %}
Documentation pages are also available as MCP resources through the `mechanic-docs://` URI scheme.
{% endhint %}

***

### `similar_tasks`

Find tasks related to a specific task by analyzing shared tags, event subscriptions, and title similarity. Useful for discovering alternative approaches or complementary automations.

**Parameters:**

* `handle` (required): Task handle or ID to find similar tasks for
* `limit` (optional): Maximum number of similar tasks to return (default: 10)

***

### `refresh_index`

Refresh and rebuild the search index from bundled data. Generally not needed as the server includes pre-built indexes.

***

## Usage best practices

When working with your AI assistant and the Mechanic MCP server:

* **Discover before building**: Search for existing tasks before creating custom solutions. The library contains 350+ battle-tested tasks.
* **Use GraphQL**: Prefer Shopify's GraphQL Admin API in task code. REST is deprecated in Mechanic.
* **Reference public URLs**: Always cite tasks using their public URLs (tasks.mechanic.dev) and docs using learn.mechanic.dev URLs.
* **Get full task details**: Use `get_task` to see complete implementations, including subscriptions, code, and options.
* **Find related work**: Use `similar_tasks` to discover alternative approaches or complementary automations.

***

## Related resources

* [Local task development with the Mechanic CLI](/resources/mechanic-cli) - Preview, diff, and publish local task files
* [Task library](/resources/task-library) - Browse and contribute to Mechanic's task library
* [Writing tasks](/core/tasks) - Learn how to write Mechanic tasks
* [Liquid templating](/platform/liquid) - Mechanic's Liquid implementation
* [Actions](/core/actions) - Available action types for tasks
* [Events and subscriptions](/core/events) - Understanding Mechanic's event system


# Mechanic Agent Skills

Install Mechanic task-writing and CLI workflow expertise into your AI coding tool.

Install Mechanic expertise directly into your AI coding tool. Mechanic's skills help an assistant write task code, work in a local task repo, preview changes, review diffs, and publish intentionally.

***

## What are Agent Skills?

Agent Skills are markdown instruction files that get installed into your AI coding tool's configuration directory. When your AI assistant starts a conversation, it reads these files automatically — giving it specialized knowledge without you having to explain anything or paste instructions.

Think of it like a reference manual that your AI always has open. The skills contain Mechanic's task-writing and local-development patterns, conventions, and common pitfalls, so your assistant knows how to produce correct task code and use the CLI safely.

***

## What the skills provide

The `mechanic-task-writer` skill gives your assistant deep knowledge of Mechanic's task patterns:

* Complete importable JSON task output — not just the Liquid code
* Correct async/sync patterns (read with `| shopify`, write with `{% action %}`)
* Preview blocks for every subscribed event topic
* GraphQL-first Shopify API usage
* Proper webhook ID handling (`admin_graphql_api_id`)
* Bulk operation patterns with `__typename` and `__parentId`
* Two-pass workflows via `mechanic/actions/perform`
* Case-insensitive tag comparison
* Task options with correct suffixes

The `mechanic-cli` skill teaches your assistant how to work safely in a local task repo:

* Initialize a repo and authenticate with an API token
* Pull existing tasks or create a blank local task with `mechanic tasks new`
* Prefer local task slugs like `order-tagger` for task commands
* Bundle helper folders before previewing or publishing
* Preview, diff, dry-run, and publish one task intentionally
* Avoid accidental repo-wide publish or large remote status checks

***

## Requirements

* **Node.js 18 or higher** installed on your system for the skill installer. The Mechanic CLI itself requires Node.js 22 or higher.
* An **AI coding tool** that supports Agent Skills, such as Claude Code, Cursor, Codex CLI, or Gemini CLI.

***

## Install the skills

### Step 1: Run the install command

Open your terminal and run:

```bash
npx skills add lightward/mechanic-skills
```

This installs both Mechanic skills. To install only one skill, use:

```bash
npx skills add lightward/mechanic-skills --skill mechanic-task-writer
```

```bash
npx skills add lightward/mechanic-skills --skill mechanic-cli
```

The CLI is interactive — it will walk you through a few prompts:

1. **Select agents.** The CLI shows a list of AI tools. Tools in the "Universal" group (Cursor, Codex, Gemini CLI, etc.) are always included. If your tool is under "Additional agents" (like Claude Code), use the arrow keys to find it and press **space** to select it, then **enter** to confirm.
2. **Choose scope.** Select **Project** (installs to your current directory) or **Global** (available in all projects).
3. **Choose install method.** Select **Symlink** (recommended — single source, easy updates) or **Copy** (independent copy).

The CLI will then place the skill file in the correct directory for each selected tool.

### Step 2: Verify the install

{% tabs %}
{% tab title="Claude Code" %}
In Claude Code, type `/skills` to see your installed skills. You should see `mechanic-task-writer` and `mechanic-cli` listed.

{% hint style="info" %}
The `/skills` command requires the native Claude Code build. If you installed Claude Code via npm, run `claude install` first to switch to the native version.
{% endhint %}

You can also check the file directly:

```bash
ls ~/.claude/skills/
```

If you installed at the project level, check `.claude/skills/` in your project directory instead.
{% endtab %}

{% tab title="Cursor" %}
Check that the skill file exists:

```bash
ls ~/.cursor/skills/
```

You should see `mechanic-task-writer` and `mechanic-cli` files or directories. If you installed at the project level, check `.agents/skills/` in your project directory instead.
{% endtab %}

{% tab title="Codex CLI" %}
In Codex, type `$skill` to see available skills. You should see `$mechanic-task-writer` and `$mechanic-cli` listed.

You can also check the file directly:

```bash
ls ~/.codex/skills/
```

{% endtab %}

{% tab title="Gemini CLI" %}
Check that the skill file exists:

```bash
ls ~/.gemini/skills/
```

You should see `mechanic-task-writer` and `mechanic-cli`. The skills may also be in `~/.agents/skills/`.
{% endtab %}

{% tab title="Manual install" %}
If the CLI doesn't detect your tool, you can copy the skill file manually from the [GitHub repository](https://github.com/lightward/mechanic-skills). Download the `SKILL.md` file and place it in your tool's skills directory.
{% endtab %}
{% endtabs %}

### Step 3: Restart your tool

Close and reopen your AI coding tool. Skills are loaded when a session starts, so you need to restart for the new skill to take effect.

***

## Using the skills

Once installed, the skills work automatically. You can use them in two ways:

* **Just ask naturally.** Say something like "help me build a Mechanic task that auto-tags orders over $100" and your assistant will use the skill's patterns automatically.
* **Use a slash command.** In Claude Code, type `/mechanic-task-writer` to explicitly invoke task-writing help, or `/mechanic-cli` for local task repo workflows. In Codex, use `$mechanic-task-writer` or `$mechanic-cli`.

For task-writing requests, your assistant will produce complete, importable task JSON with correct subscriptions, preview blocks, and GraphQL queries. In a local task repo, the `mechanic-cli` skill helps the assistant use commands like `mechanic tasks preview order-tagger`, `mechanic tasks diff order-tagger`, and `mechanic tasks publish order-tagger --dry-run` before publishing.

***

## Troubleshooting

**The command ran but nothing happened.** Check that Node.js 18+ is installed (`node --version`). The `npx skills add` command needs Node to run.

**I don't see my tool in the agent list.** Look under "Additional agents" — not all tools are in the universal group. Use the arrow keys to scroll down, then press **space** to select your tool.

**The skill file isn't in the expected directory.** The CLI places files based on which agents you selected during the interactive prompts. If it didn't detect your tool, use the manual install tab above.

**Claude Code says `/skills` is not a command.** You're on the npm version of Claude Code. Run `claude install` to switch to the native build, which supports `/skills`.

**My AI assistant doesn't seem to know Mechanic patterns.** Make sure you restarted your tool after installing. Skills are loaded at session start. Also verify the file exists in the correct directory using the verification step above.

**I want to update the skills.** Run the same install command again — it will overwrite the existing files with the latest version.

***

## Using with the MCP server

For the best results, use Agent Skills together with the [Mechanic MCP Server](/resources/mcp) and the [Mechanic CLI](/resources/mechanic-cli). The MCP server gives your AI assistant access to the task library and documentation at query time, Agent Skills provide the patterns and conventions for writing correct task code, and the CLI previews, diffs, and publishes local task files.

***

## Related resources

* [Local task development with the Mechanic CLI](/resources/mechanic-cli) — preview, diff, and publish local task files
* [Mechanic MCP Server](/resources/mcp) — connect your AI to the task library and docs
* [Using AI with Mechanic](/ai) — overview of all Mechanic AI tools
* [Task library](/resources/task-library) — browse 350+ pre-built automation tasks
* [GitHub: mechanic-skills](https://github.com/lightward/mechanic-skills) — source repository
* [skills.sh](https://skills.sh) — Agent Skills directory


# Slack community

Join the Mechanic Slack community to ask questions, share solutions, and connect with other developers and merchants.

Mechanic was made for working together. Our Slack workspace is where hundreds of folx compare implementation notes, collaborate on projects, and talk about the evolution of Mechanic itself – and it's the best place to ask your questions. You are always invited. :)

[**Join the Mechanic Slack workspace**](https://join.slack.com/t/usemechanic/shared_invite/zt-35thvnj5i-hlOy37OVU8f_QRCjcGPn~w)

{% hint style="info" %}
Got some code to share in Slack? Use [**code snippets**](https://slack.com/slack-tips/share-code-snippets) to share code with line numbers, and syntax highlighting, in a way that doesn't take up lots of vertical space in the channel. **Don't share lots of code without a snippet!**
{% endhint %}


# Tutorials

Step-by-step guides for building Mechanic tasks — from your first automation to advanced patterns.

Step-by-step guides for common Mechanic development patterns. These tutorials walk through real-world scenarios, from setting up webhooks to building scheduled data feeds.

## Written tutorials

* [Your first custom task](/resources/tutorials/your-first-custom-task) — build a complete task from scratch
* [Creating a Mechanic webhook](/resources/tutorials/creating-a-mechanic-webhook) — set up an endpoint that triggers Mechanic tasks from external services
* [Practicing writing tasks](/resources/tutorials/practicing-writing-tasks) — exercises for learning Mechanic's Liquid environment
* [Triggering tasks from a contact form](/resources/tutorials/triggering-tasks-from-a-contact-form) — connect a storefront form to a Mechanic task
* [Creating scheduled CSV feeds](/resources/tutorials/creating-scheduled-csv-feeds) — generate and deliver CSV files on a schedule
* [Running bulk operation mutations](/resources/tutorials/bulk-operation-mutations) — upload JSONL variables and run Shopify bulk mutations
* [Fetching data from a shared Google sheet](/resources/tutorials/fetching-data-from-a-shared-google-sheet) — pull spreadsheet data into a task
* [Query analytics data with ShopifyQL](/resources/tutorials/query-analytics-data-with-shopifyql) — use ShopifyQL to access analytics in Mechanic

## Video walkthroughs

For visual learners, our [video walkthroughs](/resources/tutorials/video-walkthroughs) cover common task patterns including auto-tagging, inventory sync, scheduled emails, and more.

## Getting started

New to Mechanic? Start with the [Getting started](/getting-started) guide to install Mechanic and find your first task from the library. When you're ready to build your own, work through [Your first custom task](/resources/tutorials/your-first-custom-task), then try [Practicing writing tasks](/resources/tutorials/practicing-writing-tasks) for more exercises. The [Core Concepts](/core/events) section of the docs covers the platform's execution model in detail.


# Video walkthroughs

Video tutorials for common Mechanic tasks — auto-tagging, inventory sync, email automation, and more.


# Auto-tag orders by originating staff member

{% embed url="<https://www.youtube.com/watch?v=6E-oEGeBumE&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Maintain a tag for orders processed today

{% embed url="<https://www.youtube.com/watch?v=1BzhNUZ8Efo&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Auto-tag orders with their tracking numbers

{% embed url="<https://www.youtube.com/watch?v=qNsdDaPYl24&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Sync inventory for shared SKUs

{% embed url="<https://www.youtube.com/watch?v=CoWvIdibz9Y&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Auto-tag products when their SKU(s) change

{% embed url="<https://www.youtube.com/watch?v=v0W7JZV4RBQ&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Auto-publish new products

{% embed url="<https://www.youtube.com/watch?v=yCV17WpS1Kg&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Email a report of customers who haven't ordered in X days

{% embed url="<https://www.youtube.com/watch?v=y1fV3aQrS1g&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Upgrading a Mechanic task: Adding a time delay

{% embed url="<https://www.youtube.com/watch?v=xT9j0yMXPDM&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Email the customer when tracking numbers are added to their order

{% embed url="<https://www.youtube.com/watch?v=W9VztafUY84&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Adding an optional time delay to your Mechanic task

{% embed url="<https://www.youtube.com/watch?v=X0GVvfPHQqo&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Delete all orders

{% embed url="<https://www.youtube.com/watch?v=cANLrWvOTNw&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Send an email when a specific product is shipped

{% embed url="<https://www.youtube.com/watch?v=5atvVttJnbY&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Send recurring reminders about unpaid orders

{% embed url="<https://www.youtube.com/watch?v=IhLJHfIYrgg&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Send an email when a product's price goes below its cost

{% embed url="<https://www.youtube.com/watch?v=LrFu6bV1f1o&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw&index=3>" %}


# Auto-tag customers by sales channel

{% embed url="<https://www.youtube.com/watch?v=TN13-eX1ops&list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw&index=2>" %}


# Creating products in bulk

{% embed url="<https://www.youtube.com/playlist?list=PLL9QVbRVW16vxuebeRR8ozUZIAb7ntNCw>" %}


# Your first custom task

Build your first Mechanic task from scratch — a step-by-step tutorial for auto-tagging high-value Shopify orders.

This tutorial walks you through building a complete Mechanic task from scratch. By the end, you'll have a working task that automatically tags orders over $100 with a "high-value" tag.

## Prerequisites

* A Shopify store with [Mechanic installed](https://apps.shopify.com/mechanic)
* Basic familiarity with what Mechanic does (see the [Introduction](/))

## What you'll build

When a new order comes in with a total over $100, your task will automatically add the tag "high-value" to it. This is a common pattern — tagging resources based on their properties — and it touches all the core concepts: events, subscriptions, Liquid code, previews, and actions.

## Step 1: Create a new task

1. Open Mechanic in your Shopify admin
2. Click the "Add task" button (or navigate to the "Add task" page)
3. Click the "New blank task" button
4. Give your task a name: **Auto-tag high-value orders**

You'll see the task editor with several sections: subscriptions, code, and a preview panel.

## Step 2: Add a subscription

A [subscription](/core/tasks/subscriptions) tells Mechanic which [events](/core/events) your task should respond to. Since you want to react to new orders, add this subscription:

```
shopify/orders/create
```

This means your task will run every time Shopify sends a new order event to Mechanic. Each event has a [topic](/core/events/topics) — in this case `shopify/orders/create` — and Mechanic matches it to tasks with a matching subscription.

{% hint style="info" %}
You can browse all available event topics in the [Event topics](/platform/events/topics) reference. Shopify topics correspond to Shopify webhooks; Mechanic also has its own topics for schedules, errors, and more.
{% endhint %}

## Step 3: Write the task code

Mechanic tasks are written in [Liquid](/platform/liquid/basics), a template language created by Shopify. When your task runs, Mechanic provides [environment variables](/core/tasks/code/environment-variables) — for a `shopify/orders/create` subscription, you automatically get an `order` variable containing the order data from the webhook.

Paste this into the **Code** section:

```liquid
{% if event.preview %}
  {% assign order = hash %}
  {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
  {% assign order["total_price"] = "150.00" %}
{% endif %}

{% assign threshold = 100 %}

{% assign order_total = order.total_price | times: 1.0 %}

{% if order_total > threshold %}
  {% action "shopify" %}
    mutation {
      tagsAdd(
        id: {{ order.admin_graphql_api_id | json }}
        tags: ["high-value"]
      ) {
        userErrors {
          field
          message
        }
      }
    }
  {% endaction %}
{% endif %}
```

Let's break this down piece by piece.

### Preview stub data

```liquid
{% if event.preview %}
  {% assign order = hash %}
  {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
  {% assign order["total_price"] = "150.00" %}
{% endif %}
```

[Previews](/core/tasks/previews) are how Mechanic shows you what your task will do — and how Mechanic figures out what Shopify permissions your task needs. During preview, the Shopify API is not available, so you provide [stub data](/core/tasks/previews/stub-data): a fake order with a realistic ID and a price above the threshold.

The `hash` keyword creates an empty object. You then assign properties to it, replacing the `order` variable that would normally come from the event.

{% hint style="warning" %}
The stub data ID must look like a real Shopify resource ID (e.g. `gid://shopify/Order/1234567890`). Mechanic reads this ID during preview to determine which Shopify permissions to request — in this case, `write_orders` for tagging an order. You can also explicitly declare permissions using the [`{% permissions %}`](/platform/liquid/tags/permissions) tag.
{% endhint %}

### Comparing the order total

```liquid
{% assign threshold = 100 %}
{% assign order_total = order.total_price | times: 1.0 %}

{% if order_total > threshold %}
```

The `order.total_price` value from Shopify's webhook is a string like `"150.00"`. The `times: 1.0` filter converts it to a number so you can compare it. If the total exceeds the threshold, the task proceeds to tag the order.

### The Shopify action

```liquid
{% action "shopify" %}
  mutation {
    tagsAdd(
      id: {{ order.admin_graphql_api_id | json }}
      tags: ["high-value"]
    ) {
      userErrors {
        field
        message
      }
    }
  }
{% endaction %}
```

The `{% action "shopify" %}` tag defines a [Shopify action](/core/actions/shopify) — it tells Mechanic to make a GraphQL API call to Shopify. Inside the tag, you write a GraphQL mutation. The [`tagsAdd`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/tagsAdd) mutation adds tags to any taggable resource.

The `| json` filter safely formats the order's GraphQL ID as a JSON string. Always include `userErrors` in your mutations — this is how Shopify reports problems.

{% hint style="info" %}
Actions are not performed immediately. Mechanic collects all actions from a task run, then performs them in sequence after the task finishes rendering. Learn more in [Actions](/core/actions).
{% endhint %}

## Step 4: Check the preview

After pasting the code, look at the preview panel on the right side of the editor. You should see a Shopify action containing your `tagsAdd` mutation, with the stub order ID and the "high-value" tag. This confirms two things:

1. Your task logic is working correctly
2. Mechanic knows your task needs the `write_orders` Shopify permission

If the preview is empty, double-check that your stub data sets `total_price` to a value above the threshold.

## Step 5: Save the task

Click **Save**. Mechanic will:

1. Parse your subscriptions and register for the `shopify/orders/create` webhook
2. Analyze the preview to determine required Shopify permissions
3. Prompt you to approve any new permissions it needs (like `write_orders`)

After saving, your task is live. The next time an order is placed with a total over $100, Mechanic will tag it with "high-value".

## Step 6: Test it

Place a test order in your store with a total over $100. Once Shopify processes the order, it will send a webhook to Mechanic, and your task will run automatically.

To see the results, open Mechanic's **Events** page. You'll see the incoming `shopify/orders/create` event, and you can expand it to see the task run and the action run that tagged the order. You can also verify the tag was applied by checking the order in Shopify admin.

## What you've learned

In this tutorial, you've worked with all of Mechanic's core concepts:

* **Events** — things that happen (a new order being created)
* **Subscriptions** — how a task declares interest in certain events
* **Task code** — Liquid that reads event data and decides what to do
* **Previews and stub data** — how tasks communicate their intent and permissions
* **Actions** — side effects the task produces (tagging an order via GraphQL)

## Getting help

{% hint style="info" %}
Mechanic support covers the platform and tasks from the [task library](/resources/task-library). Custom tasks you build yourself (including AI-generated tasks) are not covered by support. For help with custom task logic, ask in the [Mechanic Slack community](/resources/slack) or [hire a developer](/hire-a-developer).
{% endhint %}

## Next steps

* [Practicing writing tasks](/resources/tutorials/practicing-writing-tasks) — more exercises to build your skills
* [Writing a high-quality task](/techniques/writing-a-high-quality-task) — patterns for production-ready tasks
* [Task library](/resources/task-library) — hundreds of pre-built tasks to learn from and customize
* [Core Concepts: Tasks](/core/tasks) — full reference for task configuration


# Creating a Mechanic webhook

Webhooks are the nearly ubiquitous carriers of information to and from services across the internet - services like IFTTT, Zapier, Stripe, PayPal, JotForm, and countless more. You can use webhooks to send information from these services into Mechanic, where you can then perform any [logic](/core/tasks/code) and [actions](/core/actions) you need.

{% hint style="info" %}
This is a tutorial for getting started quickly. To learn more about webhooks themselves, see [Mechanic webhooks](/platform/webhooks).
{% endhint %}

When Mechanic receives data via a webhook, it fires off an event with the user topic of your choice. (For example, if you've set up an IFTTT webhook that sends you tweets, you might choose the Mechanic topic `user/ifttt/tweet`.) To make use of these events, create one or more tasks that subscribe to this topic. That's it!

Let's review a detailed example.

### 1. Create a Mechanic webhook.

Start by opening Mechanic from the **Apps** section of Shopify. Once in Mechanic, open **Settings**, then choose **Mechanic webhooks** from the sidebar.

<figure><img src="/files/YXG0A1KurEoydLKYJ1i2" alt="The Mechanic webhooks section in Settings, where a new webhook can be created"><figcaption></figcaption></figure>

Webhooks should be named after the service that will be sending you data, with an event topic that makes sense, using the format `user/subject/verb`.

For this example, we'll simply call ours "Example", with an event topic of "user/webhook/test".

Click the submit button to save the webhook, and use the copy button to copy the resulting webhook URL.

The URL will look something like this:

```
https://webhooks.mechanic.dev/00000000-0000-0000-0000-000000000000
```

{% hint style="info" %}
Older webhook URLs resemble `https://usemechanic.com/webhook/00...00`. This URL structure still works, but we recommend migrating to `https://webhooks.mechanic.dev/00...00` instead, for enhanced reliability.
{% endhint %}

### 2. Create a task that subscribes to your webhook event.

Back on the Mechanic homepage, click the "Add task" link.

![](/files/b3kJ8MCLCPa3Kvmgyyle)

Then, click the "Start a blank task" button.

![](/files/k7L2RpF53Fs0fH50h052)

Keeping things simple for this example, we'll title the task "Webhook test", with a subscription to "user/webhook/test" (to match the webhook configuration), and a simple [Echo action](/core/actions/echo) in the task code.

![](/files/uWbBa6j2V3Ko2sHIB85W)

Lastly, save the task.

### 3. Test your webhook.

Open <https://reqbin.com/>, and construct a request to our webhook. Here, we'll select "POST", paste in the webhook URL, and fill in a simple piece of content. (Webhooks support plain text, form-encoded content, *and* JSON; for this example, we'll use JSON.)

![](/files/lAaqLoPIIwDkUmhtlvbA)

Click the "Send" button, and you'll see a 204 response returned within ReqBin.

Over in Mechanic, watch for the new event on the "Events" page (or in the "Recent events" section of the Mechanic homepage):

![](/files/zecxwutOfoV5e3Wv9dvF)

Click on that event to see the results of our task and its echo action.

![](/files/XDV0egj6zcz2WQjrgy3K)

### 4. Connect your webhook URL to another service.

This last part is up to you! Provide the webhook URL, generated by Mechanic, to whatever service you'd like to use. When provided with this URL, the service will start sending your data over to Mechanic for processing.

That's it! :) Adjust to taste.


# Practicing writing tasks

In our own internal education, we've found that the following exercises work particularly well. They're all in sequence – the task to create for each subsequent exercise modifies the code you wrote previously.

{% hint style="info" %}
Working on getting better at task-writing? See [Writing a high-quality task](/techniques/writing-a-high-quality-task).
{% endhint %}

## Assignments

* [ ] 1\. Auto-tag customers with @gmail.com email addresses, with "gmail"
  * Configure the task's event subscriptions appropriately
  * Support case-insensitivity – recognize "@gmail.com", *and* "@Gmail.com"
  * Ignore domains with more extensions – don't tag for "@gmail.com.au"
  * Make sure that any existing tags on the customer's account are kept, not lost
  * Use the REST API for this operation
  * Use [a static preview action](/core/tasks/previews#static-preview-actions), to show the merchant a preview of what the task will do
* [ ] 2\. Move to [a dynamic preview action](/core/tasks/previews#dynamic-preview-actions), using stub data
* [ ] 3\. Remove the stub data, and add two [event preview definitions](/core/tasks/previews/events): one for a @gmail.com address, and one for another address
* [ ] 4\. Allow the merchant to configure the domain name to look for, and the tag to apply
  * Help the merchant make sure they enter a real domain name – return an error if the domain doesn't include a "."
  * Support case-insensitivity – if the merchant enters "Gmail.com", and the customer's email address ends with "@GMAIL.COM", they should still be tagged
* [ ] 5\. Allow the merchant to add any number of domain name and customer tag pairings
* [ ] 6\. Support responding to customer updates, in which the customer's email address changes
  * Remove any domain name tags that do not apply, and add the tag (if any that does apply
  * Do *not* remove any tags that contain a domain name tag – if the tag "google" should be removed, do not accidentally remove "google-foo"
  * Make this optional – allow the merchant to choose whether or not Mechanic listens for this
* [ ] 7\. Move to GraphQL for removing and adding tags
* [ ] 8\. Allow waiting a configurable number of minutes
  * Account for the customer tags or email address having changed during the waiting period
* [ ] 9\. Create a backfill mode for processing all existing customers, that the merchant can run manually
  * Use `{% for customer in shop.customers %}` for this operation
  * Note: this technique is useful for [reconciling missing events](/core/shopify/events/reconciling-missing-events)
* [ ] 10\. Move to GraphQL for scanning customers
  * See [Querying Shopify](/core/shopify/read) to learn about looping through results using cursors
* [ ] 11\. Move to GraphQL bulk operations for scanning customers


# Triggering tasks from a contact form

This tutorial walks you through setting up a custom task in Mechanic, which is called on Contact Form submission on your Shopify frontend, the contents of the form are passed to the task, which emails the contents in CSV format.

Before beginning this tutorial, here's what you'll need:

* A Shopify store, which has Mechanic installed (see [Mechanic's app store page](https://apps.shopify.com/mechanic?ref=lightward))
* A basic knowledge of Liquid ([need a refresher?](/platform/liquid/basics))
* A basic knowledge of JavaScript ([need a refresher?](https://www.w3schools.com/js/default.asp))

## The situation

We have an online store called Mario's Mushrooms, hosted on Shopify. Business is booming, and our mushrooms are being shipped all over the world. Our CEO, Mario, asks us to connect our default Shopify contact form to our legacy customer relationship management (or CRM) system. We are eager to help! While the CRM doesn't have an HTTP API, it can receive CSV imports via email, which it will then import into its database. This gives us our path forward!

## The plan

We are going to make a task in this cool Shopify app called Mechanic. ;) Here's what the task will do:

1. The task will add some JavaScript to the online Shopify store, which will capture the contents of the contact form when submitted, and then send those contents over to Mechanic via [webhook](/platform/webhooks)
2. Over on the Mechanic side, the task will receive the form contents, and format them as a CSV file
3. The task will then send an [email](/core/actions/email) to our CRM system, containing the CSV file as an attachment

## The Mechanic task

Time to build the task! Out of Mechanic's entire toolkit, here's what we'll use:

* [Online storefront JavaScript](/core/tasks/advanced-settings/javascript)
* [Mechanic webhooks](/platform/webhooks)
* [The csv Liquid filter](/platform/liquid/filters#csv)
* [The Email action](/core/actions/email)

### Step 1: Create a webhook, and connect it to a new blank task

Start with the [Creating a Mechanic webhook](/resources/tutorials/creating-a-mechanic-webhook) tutorial for this part. Webhooks should be configured with respect to the source that supplies them with data, so for this tutorial, use the webhook name "Contact Form" and the event topic "user/webhook/form".

### Step 2: Wire up the shop frontend to send form data to our webhook

We have options here! The only hard requirement is that we use a POST request to send form data to our webhook. This can be done using pure JavaScript, or using a library like jQuery, or even by using plain HTML to set the form tag's `action` attribute to our webhook URL.

For this tutorial, we'll use JavaScript. And because we're using Mechanic, we don't even have to edit the theme directly to add in our code – instead, we can use the task editor's [JavaScript](/core/tasks/advanced-settings/javascript) feature to have our code automatically loaded into the online storefront. (Under the hood, Mechanic leverages Shopify's [ScriptTag](https://shopify.dev/docs/admin-api/rest/reference/online-store/scripttag) API.)

For this tutorial, I created a development store and installed the [Debut theme](https://themes.shopify.com/themes/debut/styles/default). I use the contact form that comes with the theme as the form that submits to our webook. You can use any contact form on any theme, or create a form specifically for the purpose of submitting to our webhook.

![](/files/-MVUHKRYnUn-wAzvJ029)

First things first: we're going to make sure of the element ID, for our contact form. This will be important for writing JavaScript that addresses this form. After investigating, we discover that the form ID is "ContactForm". Easy enough!

![Here, we use Chrome's developer tools to verify the form's ID attribute.](/files/-MVhEqGhMC7ZhuVU9rcB)

Next, we're going to write some JavaScript that listens for the`submit` event of this form – functionally, this means that we're going to wire up some code to run when the form is submitted. The goal: to jump in when the form is submitted, send the form data to our webhook (which will then trigger our Mechanic task), and then allow the form to submit as usual. This way, we add Mechanic functionality without disabling the form's existing behavior.

Let's get started on our JavaScript. In your Mechanic task editor, scroll down and find the "JavaScript for Online Storefront" area. This will add this feature to our task, and we'll be given a place to add in our JavaScript, which will be automatically loaded into our shop frontend.

![](/files/fpA8Xkjb4OnRavblIkQe)

Copy in the JavaScript below, reading the comments for details on what's going on. Remember the "ContactForm" ID? Here's where we get to use it!

{% tabs %}
{% tab title="JavaScript for online storefront" %}

```javascript
// This code will be loaded on all pages of our store. So, we'll need
// to begin by seeing if the current page has a contact form on it,
// to make sure we're not causing errors by trying to modify a form
// that doesn't exist.

// The `contactForm` variable will either be our form (if it's present
// on this page), or will be null (if it isn't).
const contactForm = document.querySelector('#ContactForm');

// Before Mechanic delivers this JavaScript to the storefront, it first
// evaluates it for Liquid. This means that we get to use the `options`
// object. By using {{ options.mechanic_webhook_url__required }}, we can
// make the webhook URL configurable.
const mechanicWebhookUrl = {{ options.mechanic_webhook_url__required | json }};

// We only want to run all of this if there's a contact form on the page.
if (contactForm) {

  // Setting up a flag for later - keep reading!
  let submittedToMechanic = false;

  contactForm.addEventListener(
    'submit',
    (event) => {
      // We're going to prevent the form submit from doing its normal
      // normal. We'll re-submit the form in a second, after we've
      // submitted data to Mechanic.
      event.preventDefault();

      // We'll use fetch to make our POST request:
      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
      fetch(
        mechanicWebhookUrl,
        {
          method: 'POST', 
          body: new FormData(contactForm),
        }
      ).then((response) => {
        console.log('Sending data to Mechanic: Success!', response);
      }).catch((error) => {
        console.error('Sending data to Mechanic: Error!', error);
      }).finally(() => {
        // Now that we're done with sending our data to Mechanic,
        // we're going to manually submit the contact form. This won't
        // trigger the "submit" event again; it'll just run the form's
        // usual submit behavior.
        contactForm.submit();
      });
    },
  );
}
```

{% endtab %}
{% endtabs %}

When pasting in this code, a new task option will appear, allowing the user (that's us, for now) to configure the webhook URL. Here's where we use the Mechanic-generated webhook URL from earlier.

![](/files/m9dTaXydvXysCXJ7Hqh5)

With all that in place, save the task. We're leaving the task code empty for right now, and that's okay!

### Step 4: Receive our form submission on the Mechanic side, convert it to a CSV, and send it as an email attachment

To make sure what data we're working with, let's submit the contact form, and then examine the resulting event data in Mechanic. (It's okay that we hit the captcha prompt; the important part is making sure that we're sending data to Mechanic.)

![We've got the Chrome developer tools open so we can see our console.log messages.](/files/-MVhQ0PugZ6Qiy7nHsHR)

Heading to the "Events" page of the Mechanic app, we can see our data coming in.

![](/files/KYVDKd0cF35MQRDvl7YK)

Clicking through to that new event, we can see the event data on the right, reflecting what was in the form at the time of submission. (Depending on the nature of your specific contact form HTML, you might see something slightly different.)

![](/files/nwU8xGN6lY706mBghhDm)

This is perfect! The data we are interested in is inside of an event data property called `"contact"`. This means that, in Liquid, we can access the contact data using `{{ event.data.contact }}`.

{% hint style="info" %}
In the code sample below, we reference individual input values according to the keys we see above, in the `contact` object. We see that the phone number is stored in the `"phone"` key, so we use `event.data.contact.phone` to reference it.

When you're assembling your version of this task, make sure to update the task code to reflect the data keys you see in the incoming event.
{% endhint %}

Moving back to the task editor, the first step is to extract this data, and assemble it into something we can format using the [csv](/platform/liquid/filters#csv) filter. Because that filter is made to handle tables of data, this means that we'll create an array of "rows", and fill it with arrays of "columns", and then pass the result into the csv filter.

After that, we'll add an [Email](/core/actions/email) action, configuring it with our CSV data as an attachment. We'll also add a few more task options that will make it easy to reconfigure this task in the future, without having to touch the task code.

{% tabs %}
{% tab title="Task code" %}

```liquid
{% assign rows = array %}

{% assign header = array %}
{% assign header[0] = "Name" %}
{% assign header[1] = "Email" %}
{% assign header[2] = "Phone Number" %}
{% assign header[3] = "Message" %}
{% assign rows[rows.size] = header %}

{% assign row = array %}
{% assign row[0] = event.data.contact.name %}
{% assign row[1] = event.data.contact.email %}
{% assign row[2] = event.data.contact.phone %}
{% assign row[3] = event.data.contact.body %}
{% assign rows[rows.size] = row %}

{% assign csv_data = rows | csv %}

{% action "email" %}
  {
    "to": {{ options.recipient_email_address__email_required | json }},
    "subject": {{ options.email_subject__required | json }},
    "body": {{ options.email_body__required_multiline | strip | newline_to_br | json }},
    "attachments": {
       {{ options.csv_attachment_filename__required | replace: ".csv", "" | append: ".csv" | json }}: {{ rows | csv | json }}
     }
   }
{% endaction %}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
When writing a task, it's important to think about [previews](/core/tasks/previews), and how they appear to the user (and to Mechanic itself). This task always sends a simple email for every event it receives, and doesn't require any special permissions, so we don't need to do any preview work here. If the task only sent an email under limited conditions, or if it needed to access the Shopify API, we'd need to do more work to make sure the task generates an intentional preview.

To learn more about this, see [Previews](/core/tasks/previews).
{% endhint %}

Here's how we'll configure the task, using the task option fields that automatically appear based on our task code:

![](/files/y2MAxt9v4C1HCt0GXusY)

### Step 5: Testing

With everything assembled, we head back to the contact form, and make a submission. Back in the task editor, we see a new event appear in "Recent activity", with a green checkmark indicating that the task generated and performed an action.

![](/files/n78M5AUk37eGiCapZi1r)

## The end!

We did it! We augmented our existing contact form with the ability to send submission data to our new Mechanic task, which relays the data to our CRM system using a CSV email attachment. 🎉

Thanks for reading! If you've got questions or suggestions, join the [Mechanic Slack workspace](https://join.slack.com/t/usemechanic/shared_invite/zt-cq84nrs7-ggYbYTbf~CrCjTg8nmHP2A). :)

### Import the final task

If you'd like to quickly pull in all of the task code and configuration we used here, use this task export:

```json
{"name":"Receive contact form for CRM","options":{"recipient_email_address__email_required":"crm_imports@example.com","email_subject__required":"Contact form submission for CRM: {{ \"now\" | date: \"%Y-%m-%d %H:%M\" }}","email_body__required_multiline":"Hello,\n\nPlease find the attached CSV. Thanks!\n\n-Mechanic, for {{ shop.name }}","csv_attachment_filename__required":"contact-form-for-crm-{{ \"now\" | date: \"%s\" }}","mechanic_webhook_url__required":"https://webhooks.mechanic.dev/00000000-0000-0000-0000-000000000000"},"subscriptions":["user/webhook/form"],"subscriptions_template":null,"script":"{% assign rows = array %}\n\n{% assign header = array %}\n{% assign header[0] = \"Name\" %}\n{% assign header[1] = \"Email\" %}\n{% assign header[2] = \"Phone Number\" %}\n{% assign header[3] = \"Message\" %}\n{% assign rows[rows.size] = header %}\n\n{% assign row = array %}\n{% assign row[0] = event.data.contact.name %}\n{% assign row[1] = event.data.contact.email %}\n{% assign row[2] = event.data.contact.phone %}\n{% assign row[3] = event.data.contact.body %}\n{% assign rows[rows.size] = row %}\n\n{% assign csv_data = rows | csv %}\n\n{% action \"email\" %}\n  {\n    \"to\": {{ options.recipient_email_address__email_required | json }},\n    \"subject\": {{ options.email_subject__required | json }},\n    \"body\": {{ options.email_body__required_multiline | strip | newline_to_br | json }},\n    \"attachments\": {\n       {{ options.csv_attachment_filename__required | replace: \".csv\", \"\" | append: \".csv\" | json }}: {{ rows | csv | json }}\n     }\n   }\n{% endaction %}","docs":null,"halt_action_run_sequence_on_error":false,"liquid_profiling":false,"online_store_javascript":"// This code will be loaded on all pages of our store. So, we'll need\n// to begin by seeing if the current page has a contact form on it,\n// to make sure we're not causing errors by trying to modify a form\n// that doesn't exist.\n\n// The `contactForm` variable will either be our form (if it's present\n// on this page), or will be null (if it isn't).\nconst contactForm = document.querySelector('#ContactForm');\n\n// Before Mechanic delivers this JavaScript to the storefront, it first\n// evaluates it for Liquid. This means that we get to use the `options`\n// object. By using {{ options.mechanic_webhook_url__required }}, we can\n// make the webhook URL configurable.\nconst mechanicWebhookUrl = {{ options.mechanic_webhook_url__required | json }};\n\n// We only want to run all of this if there's a contact form on the page.\nif (contactForm) {\n\n  // Setting up a flag for later - keep reading!\n  let submittedToMechanic = false;\n  \n  contactForm.addEventListener(\n    'submit',\n    (event) => {\n      // We're going to prevent the form submit from doing its normal\n      // normal. We'll re-submit the form in a second, after we've\n      // submitted data to Mechanic.\n      event.preventDefault();\n\n      // We'll use fetch to make our POST request:\n      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API\n      fetch(\n        mechanicWebhookUrl,\n        {\n          method: 'POST', \n          body: new FormData(contactForm),\n        }\n      ).then((response) => {\n        console.log('Sending data to Mechanic: Success!', response);\n      }).catch((error) => {\n        console.error('Sending data to Mechanic: Error!', error);\n      }).finally(() => {\n        // Now that we're done with sending our data to Mechanic,\n        // we're going to manually submit the contact form. This won't\n        // trigger the \"submit\" event again; it'll just run the form's\n        // usual submit behavior.\n        contactForm.submit();\n      });\n    },\n  );\n}","order_status_javascript":null,"perform_action_runs_in_sequence":false,"shopify_api_version":"2021-01"}
```


# Creating scheduled CSV feeds

In this tutorial, you'll learn how to create a feed of your shop's data, and make it available on your online store, at a URL like `https://example.com/pages/feed`[.](https://example.com/pages/feed)

Tip: The data you generate can be imported directly into Google Sheets. Learn more: [Google Sheets integration](/platform/integrations/google-drive-and-google-sheets)

{% hint style="warning" %}
This technique has several limitations:

* Shopify doesn't support delivering the feed contents as plaintext. To get technical, this means that the feed will always be delivered with a content type of text/html.
* Because this task stores feed values as a shop metafield, feeds created with this technique may only contain and display up to 65,535 characters.

To move beyond these, consider using the [FTP action](/core/actions/ftp) to upload your feed to your own server.
{% endhint %}

## Instructions

### 1. Create your task.

Start with our example task, using the "Try this task" button to add it to your account:

[**Task: Create a product inventory feed**](https://tasks.mechanic.dev/create-a-product-inventory-feed)

Immediately after adding the task, run it by clicking the "Run task" button. This will populate your shop's records with the initial value of the feed.

This task replicates Shopify's own product inventory CSV export. Feel free to make changes to the task code, and don't hesitate to get in touch if you have questions. :)

### 2. Create a page template, called "page.feed.liquid".

This is the template that will be responsible for displaying your feed contents, without the usual page formatting that your shop's theme usually applies.

To do this, navigate to the "Themes" section of your Shopify admin (under "Online Store", or by searching for "themes"). Then, under the "Actions" menu for your current theme, click the "Edit code" link.

![](/files/c8avzKWS6cnY5YLdcg0M)

Next, click "Add a new template".

![](/files/fU5zYzfcNHdqSiDO46yh)

Then, select the option for creating a "page" template, of type "liquid", and fill in the text box with the name "feed" (or another template name to your liking).

![](/files/sm4TlvQZkjCwogM1Mdpb)

Next, fill in the template contents with the following:

```liquid
{%- layout none -%}
{{- shop.metafields.mechanic.feed -}}
```

... and click the "Save" button. Your template should look like this:

![](/files/xIw99mjk0RVoiuyUWwn9)

### 3. Create a new page to use as your feed.

Navigate to the "Pages" section of the Shopify admin (under "Online Store"), and click the "Add page" button (or search the admin for "add page"). Name the page "Feed" (or another name of your liking), and change the page template to "page.feed.liquid".

![](/files/pnnBsZNDNTu94StfdfBr)

Save the page.

### You're done!

Open up the page you just created, and you should see the contents of your feed. :) If you have any questions, head to [our community Slack](/resources/slack).


# Running bulk operation mutations

Run Shopify bulk operation mutations from a Mechanic task using staged JSONL uploads.

Shopify's `bulkOperationRunMutation` runs one mutation many times, using a JSONL file where each line contains the variables for one mutation execution. Mechanic can monitor the resulting Shopify bulk operation and return the completed JSONL result to the same task using `mechanic/shopify/bulk_operation`.

{% hint style="warning" %}
The example task below creates two test products. Run it in a development store, or adjust the mutation before using it in production.
{% endhint %}

## When to use this pattern

Use a bulk operation mutation when you need to run the same Shopify-supported and Mechanic-allowed mutation many times, and your task can wait for an asynchronous result. This is useful for high-volume imports or updates where normal one-by-one GraphQL actions would be slower or harder to throttle.

Do not use this pattern when each mutation depends on the result of the previous one, when the order of execution matters, or when your task needs the result immediately in the same run.

Shopify allows most Admin API mutations in `bulkOperationRunMutation`, except recursive bulk operation mutations such as `bulkOperationRunMutation` and `bulkOperationRunQuery` themselves. Shopify also limits the mutation string to one connection field. Mechanic still applies the Shopify action's mutation safety checks; if a mutation is not allowed in Mechanic, the action run will fail with an error.

{% hint style="info" %}
Mechanic's HTTP action uses file generators for multipart uploads, and generated files have a 20MB per-file limit. Shopify's bulk import JSONL limit may be higher than that, so split large imports into batches that fit Mechanic's generated-file limit.
{% endhint %}

## How the flow works

A bulk operation mutation has one extra step that bulk operation queries do not have: Shopify needs the variables in a staged JSONL file before the bulk mutation can start.

In Mechanic, this usually uses action result events to move through the workflow:

1. A `shopify` action calls `stagedUploadsCreate` with `resource: BULK_MUTATION_VARIABLES`.
2. A `mechanic/actions/perform` run uploads the JSONL variables file using the [HTTP action](/core/actions/http).
3. Another `mechanic/actions/perform` run calls `bulkOperationRunMutation`, using the staged upload path from the first step.
4. A final `mechanic/actions/perform` run confirms Shopify accepted the bulk mutation.
5. A `mechanic/shopify/bulk_operation` run receives the completed bulk operation and reads `bulkOperation.objects`.

Shopify requires the bulk mutation to finish within 24 hours. If a job fails or gets too large, reduce the input size and run multiple batches.

## Permissions

This example declares `read_products` and `write_products` explicitly:

```liquid
{% permissions %}
  read_products
  write_products
{% endpermissions %}
```

Mechanic can often infer permissions from Shopify actions, but staged JSONL files are uploaded directly to Shopify and are not visible to Mechanic's permission scanner. Explicit permissions are safest for bulk mutations, especially for generic mutations like `tagsAdd(id: $id)` or `metafieldsSet(metafields: $metafields)`, where the target resource IDs only appear in the JSONL file.

## Try it now

Use the importable example below if you need to test the full staged upload and bulk mutation flow.

When writing your own task from a blank task, start typing `boilerplate:bulk_operation_mutation` in the code editor to insert starter code for this full pattern. Use `boilerplate:staged_upload_jsonl` if you only need the staged upload portion.

After importing it:

1. Run the task manually.
2. Watch the task move through three `mechanic/actions/perform` stages: `create_staged_upload`, `run_bulk_mutation`, and `start_bulk_mutation`.
3. Wait for Shopify to finish the bulk operation.
4. Open the later `mechanic/shopify/bulk_operation` run and inspect the logs for line-level results.

<details>

<summary>Importable task export</summary>

```json
{
  "name": "Test Shopify bulk operation mutation",
  "docs": "Runs a full Shopify bulk operation mutation test: creates two test products using a staged JSONL upload, starts bulkOperationRunMutation, and logs the completed line-level results. Use this in a development store, or adjust the mutation before using it in production.",
  "script": "{% permissions %}\n  read_products\n  write_products\n{% endpermissions %}\n\n{% if event.topic == \"mechanic/user/trigger\" %}\n  {% assign meta = hash %}\n  {% assign meta[\"stage\"] = \"create_staged_upload\" %}\n\n  {% action \"shopify\", __meta: meta %}\n    mutation {\n      stagedUploadsCreate(input: [{\n        resource: BULK_MUTATION_VARIABLES\n        filename: \"mechanic-bulk-mutation-test.jsonl\"\n        mimeType: \"text/jsonl\"\n        httpMethod: POST\n      }]) {\n        stagedTargets {\n          url\n          parameters {\n            name\n            value\n          }\n        }\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  {% endaction %}\n\n{% elsif event.topic == \"mechanic/actions/perform\" and action.meta.stage == \"create_staged_upload\" %}\n  {% if action.run.ok == false %}\n    {% error action.run.error %}\n  {% endif %}\n\n  {% assign staged_result = action.run.result.data.stagedUploadsCreate %}\n\n  {% if staged_result.userErrors != empty %}\n    {% error staged_result.userErrors %}\n  {% endif %}\n\n  {% assign staged_target = staged_result.stagedTargets.first %}\n  {% assign upload_body = hash %}\n  {% assign staged_upload_path = nil %}\n\n  {% for parameter in staged_target.parameters %}\n    {% assign upload_body[parameter.name] = parameter.value %}\n\n    {% if parameter.name == \"key\" %}\n      {% assign staged_upload_path = parameter.value %}\n    {% endif %}\n  {% endfor %}\n\n  {% assign timestamp = \"now\" | date: \"%Y%m%d%H%M%S\" %}\n\n  {% capture variables_jsonl %}\n{\"product\":{\"title\":\"Mechanic bulk mutation test {{ timestamp }} A\"}}\n{\"product\":{\"title\":\"Mechanic bulk mutation test {{ timestamp }} B\"}}\n  {% endcapture %}\n\n  {% capture bulk_mutation %}\nmutation call($product: ProductCreateInput!) {\n  productCreate(product: $product) {\n    product {\n      id\n      title\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n  {% endcapture %}\n\n  {% assign files = hash %}\n  {% assign files[\"file\"] = variables_jsonl | strip %}\n\n  {% assign meta = hash %}\n  {% assign meta[\"stage\"] = \"run_bulk_mutation\" %}\n  {% assign meta[\"staged_upload_path\"] = staged_upload_path %}\n  {% assign meta[\"bulk_mutation\"] = bulk_mutation | strip %}\n\n  {% action \"http\", method: \"post\", url: staged_target.url, body: upload_body, files: files, __meta: meta %}\n\n{% elsif event.topic == \"mechanic/actions/perform\" and action.meta.stage == \"run_bulk_mutation\" %}\n  {% if action.run.ok == false %}\n    {% error action.run.error %}\n  {% endif %}\n\n  {% if action.run.result.status < 200 or action.run.result.status >= 300 %}\n    {% error action.run.result %}\n  {% endif %}\n\n  {% assign meta = hash %}\n  {% assign meta[\"stage\"] = \"start_bulk_mutation\" %}\n\n  {% action \"shopify\", __meta: meta %}\n    mutation {\n      bulkOperationRunMutation(\n        mutation: {{ action.meta.bulk_mutation | json }}\n        stagedUploadPath: {{ action.meta.staged_upload_path | json }}\n      ) {\n        bulkOperation {\n          id\n          status\n          type\n        }\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  {% endaction %}\n\n{% elsif event.topic == \"mechanic/actions/perform\" and action.meta.stage == \"start_bulk_mutation\" %}\n  {% if action.run.ok == false %}\n    {% error action.run.error %}\n  {% endif %}\n\n  {% assign bulk_mutation_result = action.run.result.data.bulkOperationRunMutation %}\n\n  {% if bulk_mutation_result.userErrors != empty %}\n    {% error bulk_mutation_result.userErrors %}\n  {% endif %}\n\n  {% log bulk_operation_started: bulk_mutation_result.bulkOperation %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n  {% log bulk_operation_type: bulkOperation.type %}\n  {% log bulk_operation: bulkOperation %}\n\n  {% if bulkOperation.status == \"FAILED\" %}\n    {% error bulkOperation %}\n  {% endif %}\n\n  {% for object in bulkOperation.objects %}\n    {% assign result = object.data.productCreate %}\n\n    {% if object.errors != blank %}\n      {% log bulk_mutation_graphql_errors: object %}\n    {% elsif result.userErrors != empty %}\n      {% log bulk_mutation_user_errors: object %}\n    {% else %}\n      {% log created_product: result.product %}\n    {% endif %}\n  {% endfor %}\n{% endif %}\n",
  "subscriptions": [
    "mechanic/user/trigger",
    "mechanic/actions/perform",
    "mechanic/shopify/bulk_operation"
  ],
  "subscriptions_template": "mechanic/user/trigger\nmechanic/actions/perform\nmechanic/shopify/bulk_operation",
  "options": {},
  "tags": [],
  "halt_action_run_sequence_on_error": false,
  "perform_action_runs_in_sequence": false,
  "online_store_javascript": null,
  "order_status_javascript": null,
  "preview_event_definitions": []
}
```

</details>

## The important pieces

The staged upload is created with Shopify's `BULK_MUTATION_VARIABLES` resource:

```graphql
mutation {
  stagedUploadsCreate(input: [{
    resource: BULK_MUTATION_VARIABLES
    filename: "mechanic-bulk-mutation-test.jsonl"
    mimeType: "text/jsonl"
    httpMethod: POST
  }]) {
    stagedTargets {
      url
      parameters {
        name
        value
      }
    }
    userErrors {
      field
      message
    }
  }
}
```

The JSONL file contains one variables object per line:

```json
{"product":{"title":"Mechanic bulk mutation test A"}}
{"product":{"title":"Mechanic bulk mutation test B"}}
```

Each top-level JSONL key must match a variable in the mutation string. In this example, each line has a `product` key because the mutation declares `$product`.

The HTTP action uploads that JSONL as a multipart file. The `files["file"]` key is intentional: Shopify staged uploads expect the uploaded file field to be named `file`. Do not manually set a multipart `Content-Type` header; Mechanic sets the multipart boundary for you.

```liquid
{% assign files = hash %}
{% assign files["file"] = variables_jsonl | strip %}

{% action "http", method: "post", url: staged_target.url, body: upload_body, files: files, __meta: meta %}
```

The `stagedUploadPath` value comes from the staged upload parameter named `key`.

```liquid
{% if parameter.name == "key" %}
  {% assign staged_upload_path = parameter.value %}
{% endif %}
```

After the upload succeeds, the task can start the bulk mutation:

```graphql
mutation {
  bulkOperationRunMutation(
    mutation: "mutation call($product: ProductCreateInput!) { productCreate(product: $product) { product { id title } userErrors { field message } } }"
    stagedUploadPath: "tmp/example/bulk_mutation_variables.jsonl"
  ) {
    bulkOperation {
      id
      status
      type
    }
    userErrors {
      field
      message
    }
  }
}
```

When Shopify completes the operation, Mechanic invokes the same task with `mechanic/shopify/bulk_operation`. Use `bulkOperation.type` to distinguish mutation results from query results:

```liquid
{% if bulkOperation.type == "MUTATION" %}
  {% if bulkOperation.status == "FAILED" %}
    {% error bulkOperation %}
  {% endif %}

  {% for object in bulkOperation.objects %}
    {% assign result = object.data.productCreate %}

    {% if object.errors != blank %}
      {% log bulk_mutation_graphql_errors: object %}
    {% elsif result.userErrors != empty %}
      {% log bulk_mutation_user_errors: object %}
    {% else %}
      {% log created_product: result.product %}
    {% endif %}
  {% endfor %}
{% endif %}
```

For bulk mutations, each result object is the GraphQL response for one JSONL input line. A completed bulk operation can still contain per-line `errors` or `userErrors`; check those fields before treating a line as successful. Shopify can include `__lineNumber` in the output to help connect a result back to the input line.

## Related

* [Bulk operations](/core/shopify/bulk-operations)
* [Shopify action](/core/actions/shopify)
* [HTTP action](/core/actions/http)
* [Responding to action results](/techniques/responding-to-action-results)
* [Permissions](/core/tasks/permissions)


# Fetching data from a shared Google sheet

In this tutorial, you'll learn how to publish a Google sheet to the web as a comma-separated values (CSV) file and then fetch that data from Mechanic.

## Instructions

### 1. Create a Google sheet with data.

{% hint style="info" %}
The data in the sheet should be in a format that makes sense as a CSV file. The first row should contain the column headers and there shouldn't be any data on the sheet outside of those columns.
{% endhint %}

You can either create a sheet with the sample data shown below or you can use your own data for this tutorial. Keep in mind that the column headers in the first row will be the exact keys that you need to reference in the task when iterating over the data rows for your own usage.

<figure><img src="/files/O9WYzdZUqXjUD6DqiNZY" alt=""><figcaption></figcaption></figure>

### 2. Publish the sheet to the web as a CSV.

{% hint style="warning" %}
Sharing sheets openly this way on the web so that is accessible by Mechanic works best for non-identifying data. Be sure to clean all customer-specific data and branding from your sheet data before publishing.
{% endhint %}

From the **File / Share** menu, choose the **Publish to web** option.

<div data-full-width="false"><figure><img src="/files/ENJ1xOk4NJnA5ZsTUN0s" alt=""><figcaption></figcaption></figure></div>

From the **Link** tab of the modal dialog that opens, select the sheet you wish to share and the *Comma-separated values (.csv)* option, and then click the **Publish** button.

<figure><img src="/files/RQhxCDTwX89JGfxMBZQr" alt=""><figcaption></figcaption></figure>

After clicking OK on the confirmation dialog, the modal will update to show you the URL link that you will need to copy into the demonstration task configuration settings. You can safely close this dialog window now.

<figure><img src="/files/JKhYZjDZdUsmj7a8h41E" alt=""><figcaption></figcaption></figure>

### 3. Add and configure the demonstration task.

You can either add the demonstration task using the **Try this task** button from this task library link - [Demonstration task: Fetch data from a shared Google sheet](https://tasks.mechanic.dev/demonstration-fetch-data-from-a-shared-google-sheet) - or you can add it from within the **Add task** screen inside of Mechanic.

After adding the task you should update the **Gsheet URL** option field with the link to your sheet that was generated in the prior step. Update the **Alert email recipients** with one or more email addresses where you want to be notified in case Mechanic is not able to access the shared sheet (e.g. the share is disabled).

<figure><img src="/files/mHMPuO4QxiLF4BzTGaZ2" alt=""><figcaption></figcaption></figure>

### 4. Run the task and review the output.

Run the task manually using the **Run task** button and it will run the first sequence of the task, which will make an **HTTP** request to **GET** the sheet data.

<figure><img src="/files/T15pAyucUtBGoHfOwMu9" alt=""><figcaption></figcaption></figure>

To see the results of the data retrieval you need to click on the **mechanic/actions/perform** child event after it appears.

<figure><img src="/files/stYZFstXf5STyMS7WtJ2" alt=""><figcaption></figcaption></figure>

### Next steps

Using the reference information available in these docs, write your own Mechanic task code to iterate over the rows of data (array of hashes) that is parsed from the CSV, and make useful [updates to your Shopify data](/core/actions/shopify) using the GraphQL or REST APIs.

If you have any questions, head to [our community Slack](/resources/slack).


# Query analytics data with ShopifyQL

Analytics is a powerful reporting tool available to all merchants on Shopify. Mechanic can utilize the same ShopifyQL queries that generate those reports.

This tutorial will demonstrate the basic concepts on how to use [ShopifyQL](https://help.shopify.com/en/manual/reports-and-analytics/shopify-reports/report-types/shopifyql-editor) queries in Mechanic tasks to query Shopify analytics data.

{% hint style="info" %}
Review this tutorial in conjunction with the [Demonstration: Query analytics data with ShopifyQL](https://tasks.mechanic.dev/demonstration-query-analytics-data-with-shopifyql) task.
{% endhint %}

ShopifyQL queries in Mechanic work essentially the same as other [GraphQL queries](/core/shopify/read/graphql-in-liquid), by capturing a GraphQL query and passing it to the Shopify Admin API using the [shopify](/platform/liquid/filters#shopify) Mechanic Liquid filter, and then immediately accessing the query results in the same task run.

A key difference is that there will be an additional query embedded within the [shopifyqlQuery](https://shopify.dev/docs/api/admin-graphql/latest/queries/shopifyqlQuery) query structure. This embedded query holds the [ShopifyQL syntax](https://shopify.dev/docs/api/shopifyql).

#### Basic structure of a ShopifyQL query task

```liquid
{%- capture shopifyql_query -%}
  FROM sales
  SHOW net_items_sold, gross_sales, discounts, returns, net_sales, taxes, total_sales
  WHERE product_vendor IS NOT NULL
  GROUP BY product_vendor
  DURING last_year
  ORDER BY total_sales DESC
  LIMIT 1000
{%- endcapture -%}

{% capture query %}
  {
    shopifyqlQuery(query: {{ shopifyql_query | json }}) {
      tableData {
        columns {
          displayName
          name
          dataType
          subType
        }
        rows
      }
      parseErrors
    }
  }
{% endcapture %}

{% assign result = query | shopify %}

{% assign rows = result.data.shopifyqlQuery.tableData.rows %}
{% assign columns = result.data.shopifyqlQuery.tableData.columns %}

{% log rows_count: rows.size %}
{% log rows: rows %}
{% log columns: columns %}
```

{% hint style="info" %}
Use another Liquid capture block for the ShopifyQL syntax, in lieu of trying to embed it directly within the outer GraphQL query. You may also use a multiline task options field to hold the ShopifyQL syntax.
{% endhint %}

{% hint style="info" %}
The `columns` field is mostly useful when developing your custom task, to see the field names and types returned by the query. The `rows` data returned in the query will contain the field names and data as key value pairs in each row object, so the columns field is technically not needed to work with the query results.
{% endhint %}

#### Example ShopifyQL query result (JSON)

{% code title="" %}

```json
{
  "data": {
    "shopifyqlQuery": {
      "tableData": {
        "columns": [
          {
            "displayName": "Product vendor",
            "name": "product_vendor",
            "dataType": "STRING",
            "subType": null
          },
          {
            "displayName": "Net items sold",
            "name": "net_items_sold",
            "dataType": "INTEGER",
            "subType": null
          },
          {
            "displayName": "Gross sales",
            "name": "gross_sales",
            "dataType": "MONEY",
            "subType": null
          },
          {
            "displayName": "Discounts",
            "name": "discounts",
            "dataType": "MONEY",
            "subType": null
          },
          {
            "displayName": "Returns",
            "name": "returns",
            "dataType": "MONEY",
            "subType": null
          },
          {
            "displayName": "Net sales",
            "name": "net_sales",
            "dataType": "MONEY",
            "subType": null
          },
          {
            "displayName": "Taxes",
            "name": "taxes",
            "dataType": "MONEY",
            "subType": null
          },
          {
            "displayName": "Total sales",
            "name": "total_sales",
            "dataType": "MONEY",
            "subType": null
          }
        ],
        "rows": [
          {
            "product_vendor": "ACME",
            "net_items_sold": "20",
            "gross_sales": "800.0",
            "discounts": "0",
            "returns": "-24.0",
            "net_sales": "776.00",
            "taxes": "7.76",
            "total_sales": "783.76"
          },
          {
            "product_vendor": "Company 123",
            "net_items_sold": "2",
            "gross_sales": "86.55",
            "discounts": "-30.25",
            "returns": "0",
            "net_sales": "56.3",
            "taxes": "5.63",
            "total_sales": "61.93"
          }
        ]
      }
    }
  }
}
```

{% endcode %}

The rows object will contain all of the data you need from the query. Running the task once during initial development to log out the rows result will give you the data field names you can work with.

From this point, the entire arsenal of features available in Mechanic can be used to do whatever you'd like with the data. Tag customers, orders, or products. Send data to [Airtable](/platform/integrations/airtable), [Google Sheets](/core/actions/integrations/google-sheets), or [Slack](/core/actions/integrations/slack). Generate a CSV file and send via email or upload to an FTP server. Etc.

#### Some additional development tips

* Review the existing reports available in Analytics to get ideas on how you might use ShopifyQL queries in automations
* Use Sidekick (Shopify Analytics -> Reports -> New exploration) to help you generate and validate ShopifyQL syntax
* Clean up ShopifyQL queries for use by Mechanic:
  * Remove the VISUALIZE line (this has no effect for data retrieval)
  * When using a GROUP BY clause, generally you can safely remove the WITH TOTALS qualifier, as the summed totals across groups is often not useful
  * If expecting more than 1000 rows of data, then add a LIMIT line with a value greater than needed


# Converting tasks from Shopify REST to GraphQL

Step-by-step guides for migrating Mechanic tasks from Shopify's deprecated REST API to GraphQL.

{% hint style="danger" %}
**Important Notice**

Shopify has deprecated the REST Admin API that Mechanic's REST objects depend on. The product and variant endpoints were removed on Feb 1, 2025, and Shopify is phasing out the remaining REST endpoints over time. Read more [here](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model#whats-changing) and [here](https://shopify.dev/docs/apps/build/graphql/migrate).

Use [GraphQL](/core/actions/shopify#graphql) going forward. The [product](/platform/liquid/objects/shopify/product) and [variant](/platform/liquid/objects/shopify/variant) objects no longer work. The remaining REST objects will follow as Shopify continues the migration.
{% endhint %}

These conversion tutorials will be be based on products, variants, and associated resources, but the methodologies are applicable to other type of REST resources as well.

#### At a high-level, converting Mechanic tasks from Shopify REST lookups to GraphQL queries involves:

1. **Understanding the Shopify GraphQL schema**\
   Familiarize yourself with the [Shopify GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) objects, queries, and mutations.
2. **Review how to use GraphQL in Mechanic**\
   Start [here](/core/shopify/read/graphql-in-liquid) and peruse the [task library](/resources/task-library) to see examples of GraphQL usage in tasks.
3. **Identify REST usage within a task**\
   Broadly, any usage where **one Liquid REST object** is used to reference another Liquid REST object with dot notation. This does not include fields on the original REST-like webhook resource (e.g. `product.title`).\
   \
   For the product and variant resource deprecations specifically, this includes:
   * `shop.products`
   * `shop.variants`
   * `collection.products`
   * `inventory_item.variant`
   * `inventory_level.variant`
   * `line_item.product`
   * `line_item.variant`
   * `product.collections`
   * `product.images` :asterisk\_symbol:
   * `product.metafields`
   * `product.variants` :asterisk\_symbol:
   * `variant.inventory_item`
   * `variant.inventory_levels`
   * `variant.metafields`
   * `variant.product`
4. **Field mapping**: Identify the objects, fields, and nested structures needed in GraphQL based on the existing REST usage within a task. Build and validate queries using [Shopify's GraphiQL Explorer](/platform/graphql/basics/shopify-admin-api-graphiql-explorer).
5. **Update Mechanic task code** : Replace the relevant REST calls with Mechanic-flavored Liquid GraphQL query and result objects (see the tutorials following this page for examples).
6. **Testing**: Trigger the updated task to make sure it returns the expected results and/or takes the expected actions.

{% hint style="warning" %}
The product webhook does include an array of images and variants in the product JSON which will still be accessible via dot notation. Note that these are not the same as the previously available Mechanic REST lookups for those resources.

The images and variants data arrays should be used with caution once Shopify releases [support for 2 thousand variants per product](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model#whats-changing), in conjunction with the product and variant REST endpoint deprecations. The product webhook will only include full detail for the first 100 variants. It is not yet clear what Shopify will do with images in the product webhook.
{% endhint %}


# Conversion: Single resource lookups

At its core, accessing a single resource via either API is effectively the same. Typically this involves passing the ID of the resource to the API and getting back the data for that resource.

{% code title="REST - simple product lookup" %}

```liquid
{% assign product = shop.products[product_id] %}
```

{% endcode %}

{% code title="GraphQL - simple product query" overflow="wrap" lineNumbers="true" %}

```liquid
{% capture query %}
  query {
    product(id: {{ product_id | json }}) {
      id
      # additional fields as needed
    }
  }  
{% endcapture %}

{% assign result = query | shopify %}

{% assign product = result.data.product %}
```

{% endcode %}

***

In a REST call, every field of that resource will be returned, allowing the usage of simple dot notation to utilize whichever fields are desired without first requesting them.

{% code title="REST - example product logging" lineNumbers="true" %}

```liquid
{% assign product = shop.products[product_id] %}

{% log
  title: product.title,
  status: product.status,
  type: product.product_type,
  description: product.body_html,
  tags: product.tags,
  image: product.image.src
%}
```

{% endcode %}

The equivalent query in GraphQL would need to be augmented to include the desired fields.

{% hint style="warning" %}
Occasionally, the REST and GraphQL APIs do not use the same field names. And in some cases, there are some fields with no counterpart between the APIs. Review the API docs in detail for the resource being queried to make sure the task code is using the correct field names.
{% endhint %}

{% code title="GraphQL - example product logging" lineNumbers="true" %}

```liquid
{% capture query %}
  query {
    product(id: {{ product_id | json }}) {
      id
      title
      status
      productType
      descriptionHtml
      tags
      featuredImage {
        url
      }
    }
  }  
{% endcapture %}

{% log
  title: product.title,
  status: product.status,
  type: product.productType,
  description: product.descriptionHtml,
  tags: product.tags,
  image: product.featuredImage.url
%}
```

{% endcode %}

***

This is a basic task to check a product's status, type, and tags, and then output a log entry if that product qualifies.

The preview block is only showing the fields from the REST product webhook that will be used in the task. In reality, there are about 150+ lines of detail from a product webhook which has only a single variant and image. This grows much larger as variants and images are added to the product.

{% code title="REST - Basic product tagging task" overflow="wrap" lineNumbers="true" %}

```liquid
{% if event.preview %}
  {% capture product_json %}
    {
      "admin_graphql_api_id": "gid://shopify/Product/1234567890",
      "product_type": "Widget",
      "status": "active",
      "tags": "my-tag, some-other-tag"
    }
  {% endcapture %}

  {% assign product = product_json | parse_json %}
{% endif %}

{% assign product_tags = product.tags | split: ", " %}

{% if product.status == "active" and product.product_type == "Widget" %}
  {% if product.tags contains "my-tag" %}
    {% log
      message: "This product qualifies",
      product: product
    %}
  {% endif %}
{% endif %}
```

{% endcode %}

***

The product id used in the GraphQL query below comes from the REST-like product webhook, which will still exist after the REST product endpoint deprecation.

The preview block simulates the relevant shape of the returned data, which typically matches exactly what was requested in the query. This could vary though based on the task logic following the preview block.

{% code title="GraphQL - Basic product tagging task" overflow="wrap" lineNumbers="true" %}

```liquid
{% capture query %}
  query {
    product(id: {{ product.admin_graphql_api_id | json }}) {
      status
      productType
      tags
    }
  }  
{% endcapture %}

{% if event.preview %}
  {% capture result_json %}
    {
      "data": {
        "product": {
          "id": "gid://shopify/Product/1234567890",
          "productType": "Widget",
          "status": "ACTIVE",
          "tags": [
            "my-tag",
            "some-other-tag"
          ]
        }
      }
    }
  {% endcapture %}

  {% assign result = result_json | parse_json %}
{% endif %}

{% assign product = result.data.product %}

{% if product.status == "ACTIVE" and product.productType == "Widget" %}
  {% if product.tags contains "my-tag" %}
    {% log
      message: "This product qualifies",
      product: product
    %}
  {% endif %}
{% endif %}
```

{% endcode %}

{% hint style="info" %}
To assist with generating an object query block, you can use the ["object\_query" snippet](/platform/liquid/mechanic-code-snippets#object_query) in the Mechanic code editor, and it will prompt you to choose the object type to generate a query and preview block for (e.g. product).
{% endhint %}

To see a code diff from a Mechanic library task that was recently converted in this manner, click [here](https://github.com/lightward/mechanic-tasks/pull/393/files#diff-e02b657fe67dbee68d890ad84b721837f25f6ab8c99d78ac39f28ef179478228), and review the code variations between the `{% if event.topic == "shopify/orders/create" %}` blocks.


# Conversion: Resource loops to paginated queries

A typical REST products loop in Mechanic will have the structure below. While this is a concise format to get all products in shop, its main drawback is the inability to limit or filter the number of records and fields returned. This generates a significant amount of extra data for the task to manage in memory during a task run, especially if connected resources are looped as well (e.g. variants).

{% code title="REST - shop.products resource loop" overflow="wrap" lineNumbers="true" fullWidth="false" %}

```liquid
{% for product in shop.products %}
  {% comment %}
    -- product processing here, using REST fields
  {% endcomment %}

  {% for variant in product.variants %}
    {% comment %}
      -- variant processing here, using REST fields
    {% endcomment %}
  {% endfor %}
{% endfor %}
```

{% endcode %}

***

GraphQL paginated queries work by using the same (potentially filtered) query repeatedly to retrieve resources until the end of the list is reached or the querying is terminated by code logic. In Mechanic, paginated queries are typically implemented by using an outer "for loop", with an arbitrary number of maximum loops (e.g. the **100** in `{% for n in (1..100) %}`).\
\
Within the query itself, the `first` filter limits the number of records returned in this batch, and the `after` filter will instruct which "cursor" the query should start at. This cursor will initially be set to `nil`, which indicates starting at the beginning, and it will be updated by the looping logic before the next query is run, using `{% assign cursor ... %}`

.

{% hint style="info" %}
Shopify limits most GraphQL resources to 250 records per query, so this will be the most frequent value for the `first` filter seen in tasks using paginated queries.
{% endhint %}

Finally, the `query` filter of a resources query gives the ability to drastically reduce the number of records returned, allowing for very targeted inclusion and exclusion rules (e.g. products having a certain tag). Each resource has its own list of query filters, which can be reviewed in the [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql/) docs

{% hint style="warning" %}
If a query has the potential to return a very large number of resources (including connected resources) in a shop, then a [bulk operation](/core/shopify/bulk-operations) query may be better suited than using paginated GraphQL queries.
{% endhint %}

### GraphQL Paginated Query

{% code title="GraphQL - paginated products query" overflow="wrap" lineNumbers="true" fullWidth="false" %}

```liquid
{% assign cursor = nil %}
{% assign search_query = nil %}

{% for n in (1..100) %}
  {% capture query %}
    query {
      products(
        first: 250
        after: {{ cursor | json }}
        query: {{ search_query | json }}
      ) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          id
          # relevant product fields
          variants(first: 100) {
            nodes {
              id
              # relevant variant fields
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "products": {
            "nodes": [
              {
                "id": "gid://shopify/Product/1234567890",
                "variants": {
                  "nodes": [
                    {
                      "id": "gid://shopify/ProductVariant/1234567890"
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% for product in result.data.products.nodes %}
    {% comment %}
      -- product processing here, using GraphQL fields from the query
    {% endcomment %}

    {% for variant in product.variants.nodes %}
      {% comment %}
        -- variant processing here, using GraphQL fields from the query
      {% endcomment %}      
    {% endfor %}
  {% endfor %}
  
  {% comment %}
    -- if there is another page of data, then update the cursor for the next loop
  {% endcomment %}

  {% if result.data.products.pageInfo.hasNextPage %}
    {% assign cursor = result.data.products.pageInfo.endCursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}
```

{% endcode %}

{% hint style="info" %}
To assist with generating a paginated query block, you can use the ["paginated\_query" snippet](/platform/liquid/mechanic-code-snippets#paginated_query) in the Mechanic code editor, and it will prompt you to choose the object type to paginate over (e.g. products).
{% endhint %}

To see a code diff from a Mechanic library task that was recently converted in this manner, click [here](https://github.com/lightward/mechanic-tasks/pull/393/files#diff-2efeafa8d41fb00ed8ffcd8481f358850d69d8b7537364d17cc744ec9f357681).


# Conversion: Connections from a resource

Until further notice, Shopify will continue to send product webhook data in a REST-like format. Tasks that **only** use the fields available in the webhook (e.g. `product.title`) may not need to be converted by the deprecation notice date. However, if connections to other resources are made from that product (e.g. `product.collections`), then that will require conversion.

This is a simple task to loop through a product's collections, check if the collection contains a certain tag, then log out the collection title.

{% code title="REST - Looping through a product" overflow="wrap" lineNumbers="true" %}

```liquid

{% for collection in product.collections %}
  {% assign collection_tags = collection.tags | split: ", " %}

  {% if collection_tags contains "my-tag" %}
    {% log collection_with_my_tag: collection.title %}
  {% endif %}
{% endfor %}
```

{% endcode %}

***

The GraphQL version of the the task above use a paginated query to get all of the collections a product is a member of. The outer loop upper range (e.g. the **10** in `{% for n in (1..10) %}`) is arbitrary, and you may adjust it to the approximate maximum number of collections any given product might have.

The event preview block in this task sample makes this code appear to be overly verbose, however the [preview block](/core/tasks/previews/stub-data#stubbing-graphql-data) is often an important step to ensure that Mechanic prompts for the correct scopes for reading and writing Shopify API data.

{% code title="GraphQL - Querying a product" overflow="wrap" lineNumbers="true" %}

```liquid
{% assign cursor = nil %}

{% for n in (1..10) %}
  {% capture query %}
    query {
      product(id: {{ product.admin_graphql_api_id | json }}) {
        collections(
          first: 250
          after: {{ cursor | json }}
        ) {
          pageInfo {
            hasNextPage
            endCursor
          }
          nodes {
            id
            title
            tags
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "products": {
            "nodes": [
              {
                "collections": {
                  "nodes": [
                    {
                      "id": "gid://shopify/Collection/1234567890",
                      "title": "Widget collection",
                      "tags": ["my-tag"]
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% for collection in result.data.product.collections.nodes %}
    {% if collection.tags contains "my-tag" %}
      {% log collection_with_my_tag: collection.title %}
    {% endif %}
  {% endfor %}

  {% if result.data.products.pageInfo.hasNextPage %}
    {% assign cursor = result.data.products.pageInfo.endCursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}
```

{% endcode %}

{% hint style="info" %}
To assist with generating a paginated query block, you can use the ["paginated\_query" snippet](/platform/liquid/mechanic-code-snippets#paginated_query) in the Mechanic code editor, and it will prompt you to choose the object type to paginate over (e.g. products).
{% endhint %}


# Conversion: Metafield lookups from a resource

For every Shopify resource object that supports metafields, Mechanic has traditionally provided a way to directly access those metafields from the resource using [dot notation](/platform/liquid/objects/shopify/metafields/metafield-collection). This shortcut is no longer accessible for product and variant REST resources, which were deprecated on Feb 1, 2025.

{% code title="REST - product metafield value check" lineNumbers="true" %}

```liquid
{% assign metafield = product.metafields.custom.my_field %}

{% if metafield.value == "Alpha" %}
  {% log "metafield value matched" %}
{% endif %}
```

{% endcode %}

While metafields can be queried directly using their ID, this attribute is not present in the product webhook data. The standard approach in GraphQL is to query the product resource for the metafield(s) and value(s), passing the `namespace` and `key` as the "key" value, in the same manner as the REST dot notation lookup.

{% code title="GraphQL - product query with metafield and value check" lineNumbers="true" %}

```liquid
{% capture query %}
  query {
    product(id: {{ product.admin_graphql_api_id | json }}) {
      metafield(key: "custom.my_field") {
        value
      }
    }
  }  
{% endcapture %}

{% assign result = query | shopify %}

{% assign metafield = result.data.product.metafield %}

{% if metafield.value == "Alpha" %}
  {% log "metafield value matched" %}
{% endif %}
```

{% endcode %}


# Conversion: Resource lookups in task option fields

An oft utilized feature of Mechanic is the ability to add Liquid tags into task options fields, such as a configurable email body. Additionally, these Liquid tags (currently) support inline resource lookups for data not available in the event webhook. However, for products and variants this no longer works as of the [Feb 1, 2025 REST deprecation](/platform/liquid/objects/shopify).

{% code title="REST - product resource lookup from line item" %}

```liquid
{%- assign qualifying_product = nil -%}

{%- for line_item in order.line_items -%}
  {%- if line_item.product.product_type == "Special" -%}
    {% assign qualifying_product = line_item.product -%}
    {%- break -%}
  {%- endif -%}
{%- endfor -%}

{%- if qualifying_product != blank -%}
  Special product notice for {{ qualifying_product.title }}...
{%- endif -%}
```

{% endcode %}

The code above could be utilized directly in a [multiline task option field](/core/tasks/options#3-flags), and it would output a string of text (e.g. "Special product notice for Widget - Red...") into the assigned option field variable.

One method of conversion for lookup fields is to utilize a GraphQL query *directly in the option field*, which naturally has some caveats.

{% code title="GraphQL - order query with line item products" lineNumbers="true" %}

```liquid
{%- assign order_id = order.admin_graphql_api_id | default: "gid://shopify/Order/12345" -%}

{%- capture query -%}
  query {
    order(id: {{ order_id | json }}) {
      id
      lineItems(first: 250) {
        nodes {
          id
          product {
            title
            productType
          }
        }
      }
    }
  }
{%- endcapture -%}

{%- assign result = query | shopify -%}

{%- assign qualifying_product = nil -%}

{%- for line_item in result.data.order.lineItems.nodes -%}
  {%- if line_item.product.productType == "Special" -%}
    {% assign qualifying_product = line_item.product -%}
    {%- break -%}
  {%- endif -%}
{%- endfor -%}

{%- if qualifying_product != blank -%}
  Special product notice for {{ qualifying_product.title }}...
{%- endif -%}
```

{% endcode %}

{% hint style="warning" %}
Event preview blocks are not evaluated in task option fields. Instead, default values should be assigned to any webhook fields utilized by the query (e.g. *product.admin\_graphql\_api\_id*). This will keep the task parser happy and allow you to save the task. Be careful though to not assign a default value to a webhook field that can have a null or blank string as a valid value.
{% endhint %}

It can be helpful when using a GraphQL query in a task option field to add the code flag to the option field, which will add line numbers and give access to Mechanic code snippets.

```liquid
{% assign email_body = options.email_body__multiline_code_required | strip | newline_to_br %}
```

<div><figure><img src="/files/OEIRb0O8V5IfNevsSfRy" alt=""><figcaption><p>Email body task option</p></figcaption></figure> <figure><img src="/files/H95VfU3T1yS6lXhnde8c" alt=""><figcaption><p>Email body task option using code flag</p></figcaption></figure></div>

{% hint style="info" %}
The embedded GraphQL query will work without or without using the "code" flag.
{% endhint %}


# Events

Events are triggers that start Mechanic tasks — Shopify webhooks, schedules, or custom sources.

An **event** represents something that happened — a new order, a product update, midnight rolling around, or a webhook arriving from an external service. Events are the entry point to all Mechanic automation: when an event occurs, Mechanic checks which tasks are subscribed to it and runs them. Tasks can also emit new events, making it possible to build multi-step workflows where one task's output drives another.

An event always has a [**topic**](/core/events/topics) and **data** (even if the data is null/nil). Event attributes may be referenced in Liquid using the [**Event object**](/platform/liquid/objects/event).

## Where events come from

Events reach Mechanic from several sources:

* **Shopify webhooks** — Shopify sends events when something changes in your store. For example, `shopify/orders/create` fires when a new order is placed, and `shopify/products/update` fires when a product is edited. These are the most common event source.
* **Schedules** — Mechanic can generate events on a schedule. A topic like [`mechanic/scheduler/daily`](/platform/events/topics#scheduler) fires once per day, useful for reporting, cleanup, or monitoring tasks.
* **On-demand runs** — Tasks can be run manually using `mechanic/user` topics. Topics like `mechanic/user/trigger`, `mechanic/user/text`, and `mechanic/user/form` add a "Run task" button in the Mechanic app. Topics like `mechanic/user/order` respond to [Shopify admin action links](/core/shopify/admin-action-links).
* **Custom events** — Developers can define their own event topics in the `user/` domain (e.g. `user/orders/export`). Tasks can generate them with the [Event action](/core/actions/event), which is useful for chaining tasks together in multi-step workflows. [Mechanic webhooks](/platform/webhooks) can generate them from external HTTP requests, and [custom Shopify webhooks](/platform/shopify/custom-webhooks) can generate them from filtered or customized Shopify webhook deliveries.

Each event carries **data** that tasks can use. For a Shopify webhook event, this is the resource data sent by Shopify (e.g. the order object). For scheduled events, the data is minimal. Tasks access this data through the [Event object](/platform/liquid/objects/event) in Liquid.

For a complete reference of available event topics, see the [Event topic list](/platform/events/topics).

## What happens next

When an event arrives, Mechanic creates a [**run**](/core/runs) to process it. Each run evaluates every [task](/core/tasks) that subscribes to that event's topic. If a task generates [actions](/core/actions) (like a Shopify API call or an email), those actions are queued and performed after the task run completes.

This is the core execution flow: **Event** → **Task Run** → **Action Run(s)**. To learn more, see [Runs](/core/runs).

{% hint style="info" %}
Incoming events may be selectively skipped using [event filters](/platform/events/filters). For handling high-frequency events, see [Debouncing events](/techniques/debouncing-events).
{% endhint %}


# How event topics work

Reference for Mechanic event topics — Shopify webhook events, scheduled events, user-triggered events, and custom event topics.

To make events easy to identify, each event has a **topic**. Tasks signal their interest in specific event topics using [**subscriptions**](/core/tasks/subscriptions).

A topic looks like "shopify/customers/create", and it has three parts:

* The **domain** describes the source of the event. Shopify events have "shopify" as their domain, and events generated by Mechanic itself use "mechanic".
* The **subject** describes the type of resource the event describes. Events that are about customers have "customers" as their subject, and events that are about orders have "orders".
* The **verb** describes what has just occurred. Events that are about creating resources generally have "create" as their verb, and events that are about deleting resources generally have "delete".

{% hint style="info" %}
[Looking for the event topic list? Start here.](/platform/events/topics) Need Shopify topics? Jump to [Shopify](/platform/events/topics#shopify).
{% endhint %}

## User-defined topics

The User event domain is for custom, user-generated events, having any subject and verb (e.g. "user/foo/bar"). As with all events, a User event topic must use the standard three-part topic form, but only the "user/" prefix is mandatory.

Mechanic allows developers several ways to generate custom User events:

* The [Event action](/core/actions/event) can be used with any User event topic
* [Mechanic webhooks](/platform/webhooks) may be configured to generate events using any User event topic
* [Custom Shopify webhooks](/platform/shopify/custom-webhooks) deliver Shopify webhook events onto a User event topic, with optional filtering and payload customization. For these events, `event.topic` is the `user/...` topic and `event.shopify_topic` stores the source Shopify topic; on native Shopify deliveries, those two values match.


# Parent and child events

In specific cases, events may be triggered by activity associated with an earlier event. In these scenarios, we describe the subsequent event as a **child event**, and the preceding event as a **parent event**.

* The [Event action](/core/actions/event) generates a new child event, when performed
* A subscription to the [mechanic/actions/perform](/techniques/responding-to-action-results) topic generates new child events as actions are performed

Tasks responding to child events may reference to the parent's event using `{{ event.parent }}`. Parent events are recursively available (as in `{{ event.parent.parent.parent }}`), to a limit of 5 generations back.

When viewing any given event in Mechanic, look in the event details to find any parent or child relationships that apply. Click through to any displayed parent or child event to view that event's details.

![](/files/n1JfRYeoFuuQ53rknSfe)

## Example

**Subscriptions**

```
mechanic/user/trigger
user/fan/out
```

**Code**

```liquid
{% assign n = event.data | default: 0 | times: 1 %}

{% if n < 5 %}
  {% for m in (0..n) %}
    {% action "event" %}
      {
        "topic": "user/fan/out",
        "data": {{ n | plus: 1 | json }},
        "task_id": {{ task.id | json }}
      }
    {% endaction %}
  {% endfor %}
{% else %}
  {% action "echo", event_data: event.data, parent_event_data: event.parent.data %}
{% endif %}
```

As written, this task will "fan out": it will generate 1 child event, which will then generate 2 child events, each of which will then generate 3 child events, and each of those will then generate 4 child events, and finally, each of those events will generate 5 child events of their own. The result: 154 events, created with a single click. 💪

Importantly, note the `"task_id"` option, applied to the Event action. This option ensures that only this task, and no other, will respond to the new event. While it's unlikely that any other task will subscribe to "user/fan/out" events, this option is important for ensuring expected behavior.


# Tasks

Tasks are the building blocks of Shopify automation in Mechanic — Liquid code that responds to events and performs actions like tagging, emailing, and syncing data.

A task is an automation unit — it watches for something to happen in your store and responds with one or more actions. A task might tag every order over $100, send a notification when inventory is low, or sync customer data to a spreadsheet.

Under the hood, a **task** combines Liquid code with user-configurable [**options**](/core/tasks/options) and a set of event [**subscriptions**](/core/tasks/subscriptions). When an event matches a subscription, Mechanic executes the task's code with access to the event data. The code can query the Shopify Admin API, apply conditional logic, iterate over data, and render any number of [**actions**](/core/actions) — Shopify mutations, emails, HTTP requests, file generation, and more.

A task uses its [**preview**](/core/tasks/previews) to communicate ahead of time the work it intends to do. Previews are important for users, and are also important for Mechanic itself – Mechanic looks to the task preview to understand what [**permissions**](/core/tasks/permissions) a task requires. Tasks can also declare their permissions explicitly using the [`{% permissions %}` tag](/platform/liquid/tags/permissions).

Tasks may be written from scratch, or installed from the Mechanic library (available in-app and [on GitHub](https://github.com/lightward/mechanic-tasks)). Once installed, a task's code may be modified at any time.

{% hint style="info" %}
Working on getting better at task-writing? See [Practicing writing tasks](/resources/tutorials/practicing-writing-tasks), and [Writing a high-quality task](/techniques/writing-a-high-quality-task).
{% endhint %}

## Example

This very basic task subscribes to shopify/customers/create, and renders an [Email action](/core/actions/email), using an email subject and body taken from user-configured [options](/core/tasks/options).

**Subscriptions**

```
shopify/customers/create
```

**Code**

```liquid
{% action "email" %}
  {
    "to": {{ options.email_recipient__email_required | json }},
    "subject": {{ options.email_subject__required | json }},
    "body": {{ options.email_body__multiline_required | newline_to_br | json }},
    "from_display_name": {{ shop.name | json }}
  }
{% endaction %}
```

**Export**

```
{"name":"Customer signup alerts","options":{"email_recipient__email_required":"aesha@example.com","email_subject__required":"A new customer has signed up: {{ customer.email }}","email_body__multiline_required":"Hi! View this customer's details online:\n\nhttps://{{ shop.domain }}/admin/customers/{{ customer.id }}\n\n-Mechanic"},"script":"{% action \"email\" %}\n  {\n    \"to\": {{ options.email_recipient__email_required | json }},\n    \"subject\": {{ options.email_subject__required | json }},\n    \"body\": {{ options.email_body__multiline_required | newline_to_br | json }},\n    \"from_display_name\": {{ shop.name | json }}\n  }\n{% endaction %}","subscriptions":["shopify/customers/create"],"online_store_javascript":null,"order_status_javascript":null,"docs":null,"subscriptions_template":"shopify/customers/create","shopify_api_version":"2022-04","liquid_profiling":false,"perform_action_runs_in_sequence":false,"halt_action_run_sequence_on_error":false,"preview_event_definitions":[]}
```

![](/files/euca22aqo9QMeDVtbLMW)


# Subscriptions

Configure Mechanic task subscriptions to respond to Shopify webhooks, scheduled events, and custom triggers — with optional time delays.

A task **subscription** is the expression of a task's intent to receive certain [**events**](/core/events), filtering by [**topic**](/core/events/topics). A subscription consists of an event topic, optionally combined with a time **offset**, which creates a delay.

A task may have any number of subscriptions.

![](/files/5c40dJ4VX2gpWQ4xZ3qy)

{% hint style="info" %}
Subscriptions to `user/...` topics may be backed by a [custom Shopify webhook](/platform/shopify/custom-webhooks), which sends Shopify-side filtered or trimmed deliveries onto a Mechanic topic of your choosing. Events arriving from a custom Shopify webhook carry `event.shopify_topic` (the source Shopify topic in canonical form, e.g. `shopify/products/update`) and have `event.source` set to `custom_shopify_webhook_subscription:<uuid>`. Mechanic also builds the usual Shopify subject variable from the source topic, such as `product` for `shopify/products/update` or `metaobject` for `shopify/metaobjects/update`.
{% endhint %}

## Delays

... are accomplished using subscription offsets, as described below. This heading is here for folks searching for a way to delay their tasks. ;)

## Offsets

A subscription offset (sometimes called a delay) defines the amount of time a task should wait or delay (!!) before responding to the incoming event. It's the easiest way to add a delay to a task's subscription to a specific topic. (For finer control over event timing, try using the `run_at` option of the [Event action](/core/actions/event).)

Subscription offsets are appended to the subscription topic, and are of the form "+1.hour". Offsets may be given using seconds, minutes, hours, days, weeks, months, or years. There is no limit to how large the subscription offset may be.

**A subscription with an offset looks like `shopify/customers/create+1.hour`.**

To learn more about scheduling work with Mechanic, see [Scheduling](/core/runs/scheduling).

{% hint style="success" %}
In practice, large offsets can make debugging difficult! If you're thinking about work to be done weeks or months or years from now, consider running an hourly or daily task that scans for work that's due to be done, instead of scheduling tasks for the distant future.
{% endhint %}

{% hint style="info" %}
In some cases, the first task run on a new mechanic/scheduler/daily task may not be performed when expected.

To illustrate: if a user creates a task at 9am Monday, subscribing to mechanic/scheduler/daily+10.hours, they will have to wait until *the following midnight* before the mechanic/scheduler/daily event is created. When that event's run is performed, the task's subscription offset will be calculated and applied, and the task run will be enqueued for 10 hours later. This means that the task will run for the first time on 10am Tuesday, *not* 10am Monday.
{% endhint %}

{% hint style="warning" %}
The \[Shopify variables]\(code/environment-variables.md#shopify-variables) available to tasks always contain data drawn from the event itself. If a task has a offset event subscription, this data may be outdated by the time the task runs.

To reload the data in a Shopify variable, use something like this:

```liquid
{% unless event.preview %}
  {% assign customer = customer.reload %}
{% endunless %}
```

Remember, Mechanic does not permit access to the Shopify API during [event preview](/core/tasks/previews). Using this `unless` statement ensures that reloading only happens during a live event.
{% endhint %}

## Using Liquid

A task's subscriptions are parsed for Liquid, at the time the task is saved. Combined with [**task options**](/core/tasks/options), this is an opportunity to generate subscriptions based on user configuration, adding or removing subscriptions based on the user's choice, or adjusting subscription offset based on a user-entered value.

One subscription is permitted per line. Blank lines and leading/trailing whitespace are permitted.

### Examples

{% tabs %}
{% tab title="Conditional subscription" %}

```liquid
shopify/orders/create

{% if options.send_email_when_order_cancelled__boolean %}
  shopify/orders/cancelled
{% endif %}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Dynamic offset" %}

```liquid
shopify/orders/paid+{{ options.days_to_wait_before_followup__number_required }}.days
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Optional offset" %}

```liquid
shopify/customers/create{% if options.wait_one_hour__boolean %}+1.hour{% endif %}
```

{% endtab %}
{% endtabs %}


# Code

Write task code in Mechanic's Liquid environment — render actions, logs, and errors in response to events.

A task's code is a [**Liquid**](/platform/liquid) template. In the same way that a Shopify storefront might use a Liquid template to receive requests and render HTML, a task uses its Liquid code to receive events, and render a series of JSON objects. These JSON objects define [**actions**](/core/tasks/code/action-objects), [**logs**](/core/tasks/code/log-objects), and [**errors**](/core/tasks/code/error-objects).

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

Task code always has access to a set of [**environment variables**](/core/tasks/code/environment-variables), which can be used to make decisions about what JSON objects to render.

A task must purposefully consider its [**preview**](/core/tasks/previews), so as to accurately communicate its intent to users and to the Mechanic platform.

{% hint style="info" %}
To find many examples of task code, browse <https://github.com/lightward/mechanic-tasks>.
{% endhint %}

## See also

* [Liquid basics](/platform/liquid/basics) — syntax, data types, variables, filters, and control flow
* [Mechanic filters](/platform/liquid/filters) — Mechanic-specific filters for data manipulation
* [Mechanic tags](/platform/liquid/tags) — `{% raw %}{% action %}{% endraw %}`, `{% raw %}{% log %}{% endraw %}`, `{% raw %}{% error %}{% endraw %}`, and more
* [Mechanic objects](/platform/liquid/objects) — event, task, options, cache, and action objects available in your code
* [Writing a high-quality task](/techniques/writing-a-high-quality-task) — patterns and practices for reliable Mechanic tasks


# Environment variables

A task's Liquid code always has access to a set of **environment variables**, defined by Mechanic.

{% hint style="info" %}
Environment variables may be reassigned as needed. (When preparing a task preview, this may be a necessary technique.) To learn more, see [Stub data](/core/tasks/previews/stub-data).
{% endhint %}

| Variable  | Contents                                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shop`    | An object containing [Shopify's REST representation of the current Shopify store](https://shopify.dev/docs/admin-api/rest/reference/store-properties/shop)    |
| `event`   | An object containing information about the current event                                                                                                      |
| `cache`   | The current store's [Mechanic cache](/platform/liquid/objects/cache) object, supporting lookups for cached values                                             |
| `globals` | The current store's visible [globals](/platform/liquid/objects/globals), for reusable JSON configuration                                                      |
| `secrets` | The current store's write-only [secrets](/platform/liquid/objects/secrets), returned as opaque references unless used by a supported action or signing filter |
| `task`    | An object containing information about the current task                                                                                                       |
| `options` | An object containing task [**options**](/core/tasks/options), configured by the user                                                                          |

{% hint style="info" %}
The \[Shopify variables]\(environment-variables.md#shopify-variables) available to tasks always contain data drawn from the event itself. If a task has a offset event subscription, this data may be outdated by the time the task runs.

To reload the data in a Shopify variable, use something like this:

```liquid
{% unless event.preview %}
  {% assign customer = customer.reload %}
{% endunless %}
```

Remember, Mechanic does not permit access to the Shopify API during [event preview](/core/tasks/previews). Using this `unless` statement ensures that reloading only happens during a live event.
{% endhint %}

## Event subject variables

When a task is actually invoked for an event, it may have access to an additional variable, determined by the specific event it is responding to. When this is the case, the additional variable will be named after the event subject, and its contents will be established by the event's data. The name of this variable is communicated by the Mechanic task editor, based on the task's current [**subscriptions**](/core/tasks/subscriptions).

For example, a subscription to shopify/customers/create will make available a variable called `customer`. A subscription to shopify/products/update will expose a variable called `product`, etc.

## Shopify variables

All Shopify events support an additional variable named after the event topic. For example, when a task responds to a shopify/customers/create event, it will have access to an additional variable named `customer`, containing the customer data contained in the event.

Shopify events always contain data from Shopify's REST representation of each resource; therefore, automatic Shopify variables always contain data from the REST representation as well. The best resource for the data available for each variable type is [Shopify's REST Admin API reference](https://shopify.dev/docs/admin-api/rest/reference).

{% hint style="warning" %}
**Shopify variables in Mechanic do not necessarily contain the same attributes as Liquid variables used in Shopify (in places like themes or email templates) – even if they share the same name.**

In Mechanic, Shopify variables always contain data from Shopify events, which are delivered to Mechanic via webhook. This means that Shopify variables always have the same data structure as Shopify webhooks, corresponding to Shopify's REST representation for this data.

For example, while Shopify themes support `customer.name`, Mechanic does not (because [Shopify's REST representation of the customer resource](https://shopify.dev/docs/admin-api/rest/reference/customers/customer) does not contain a "name" property). On the other hand, Mechanic supports `customer.created_at`, while Shopify themes do not.
{% endhint %}


# Action objects

An action object defines work to be performed by an [**action**](/core/actions), after the task is fully finished rendering. Action objects are most easily generated using the [**action tag**](/platform/liquid/tags/action).

An action object is a plain JSON object, having the following structure:

```json
{
  "action": {
    "type": ACTION_TYPE,
    "options": ACTION_OPTIONS,
    "meta": ACTION_META
  }
}
```

{% hint style="info" %}
Use the [action tag](/platform/liquid/tags/action) to skip the boilerplate while writing actions. All tasks in the Mechanic task library use the action tag, rather than writing out the action object in raw JSON.
{% endhint %}

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

## Defining an action

### Type

The action **type** is always a string, having a value that corresponds to [a supported action](/core/actions) (e.g. `"shopify"`, or `"http"`).

### Options

Action **options** vary by action type. Depending on the action type, its options may be another complete object, or an array, or a scalar value.

#### Control parameters

Mechanic reserves a small set of double-underscore parameters for controlling how an action is handled. These control parameters include:

* `__perform_event`, which you can set to `false` to skip emitting the follow-up `mechanic/actions/perform` event for that specific action (the default is to emit it).
* `__meta`, which moves the provided value into the action's `meta` field so you can attach meta data while using any action tag syntax.

Examples:

```liquid
{%- action "http", method: "get", url: "https://postman-echo.com/get", __perform_event: false -%}
```

```liquid
{%- assign meta = hash -%}
{%- assign meta["source"] = "cache" -%}
{%- action "cache", "set", "foo", "bar", __meta: meta -%}
```

### Meta

Actions may optionally include **meta** information, annotating the action with any JSON value.

When you're using the action tag, you can attach meta in a few ways:

* Provide a `meta` object alongside `options` when you're supplying an options hash.
* Supply meta as the second positional argument when the options are a hash.
* Use the `__meta` control parameter with any action tag syntax to move the value into the action's meta.

```liquid
{%- assign options = hash -%}
{%- assign options["method"] = "post" -%}
{%- assign options["url"] = "https://postman-echo.com/post" -%}
{%- assign meta = hash -%}
{%- assign meta["mode"] = "initial_request" -%}
{%- action "http", options: options, meta: meta -%}
```

```liquid
{%- assign options = hash -%}
{%- assign options["method"] = "post" -%}
{%- assign options["url"] = "https://postman-echo.com/post" -%}
{%- assign meta = hash -%}
{%- assign meta["mode"] = "initial_request" -%}
{%- action "http", options, meta -%}
```

```liquid
{%- assign meta = hash -%}
{%- assign meta["note"] = "graphql" -%}
{%- action "shopify", __meta: meta -%}
  mutation {
    tagsAdd(id: "gid://shopify/Customer/1234567890", tags: ["vip"]) {
      node { id }
      userErrors { field message }
    }
  }
{%- endaction -%}
```

This information could be purely for record-keeping, making it easy to determine why an action was rendered, or to add helpful context:

```json
{
  "action": {
    "type": "shopify",
    "options": [
      "post",
      "/admin/customers/1234567890/send_invite.json",
      {}
    ],
    "meta": {
      "invite_reason": "alpha",
      "customer_email": "customer@example.com"
    }
  }
}
```

Or, this information could be used to facilitate complex task flows, in concert with a subscription to mechanic/actions/perform (see [Responding to action results](/techniques/responding-to-action-results)). An action's meta information can supply followup task runs with information about state, allowing the task to cycle between different phases of operation.

```liquid
{% if event.topic contains "trigger" %}
  {% action %}
    {
      "type": "cache",
      "options": ["set", "foo", "bar"],
      "meta": {
        "mode": "first"
      }
    }
  {% endaction %}
{% elsif action.meta.mode == "first" %}
  {% action %}
    {
      "type": "cache",
      "options": ["set", "foo", "bar"],
      "meta": {
        "mode": "second"
      }
    }
  {% endaction %}
{% elsif action.meta.mode == "second" %}
  {% action "echo", "done" %}
{% endif %}
```


# Error objects

When a task renders an error object, the task run will be marked as failed, and no rendered action runs will be performed. This is a good way to communicate an intentional failure to the user, when your Liquid code detects a certain condition.

A task that renders an error object [during preview](/core/tasks/previews) will interrupt the preview, and visibly communicate the error to the user. This makes error objects a useful way to validate [task options](/core/tasks/options).

{% hint style="warning" %}
Unlike a "raised" exception in other programming languages, a rendered error object is simply added to the list of the task run's JSON objects. At the completion of task code rendering, all objects are evaluated; at that point, if an error object is among them, the error is then raised and shown to the user.

An error object does not halt rendering of the task's Liquid code, but it does prevent any other rendered objects from having an effect. Specifically, this means that the presence of an error object means that any action objects will be ignored.

This also means that rendering an error object will not prevent the task from reaching any syntax errors (or other problematic code) later on in the task's Liquid code.
{% endhint %}

An error object is a plain JSON object, having the following structure:

```json
{
  "error": ERROR_DETAILS
}
```

The error details can be any JSON value. This value will be represented to the user as the reason for the task failing.

Error objects are most easily generated using the [**error tag**](/platform/liquid/tags/error).


# Log objects

Log objects are useful for recording information for later reference. They have no side-effects. Carefully chosen log objects can massively simplify post-hoc debugging, especially (as we've found) when investigating merchant bug reports.

A log object is a plain JSON object, having the following structure:

```json
{
  "log": LOG_DETAILS
}
```

The log details can be any JSON value.

Log objects are most easily generated using the [**log tag**](/platform/liquid/tags/log).

Log objects appear wherever task run results are visible, including the task preview and when viewing an event.

<figure><img src="/files/3mzZfQ6SfNTnr6IEmE99" alt=""><figcaption><p>A log object visible in a task preview</p></figcaption></figure>

<figure><img src="/files/AWi5nUiE01m3xN5zrJ6q" alt=""><figcaption><p>A log object visible in a task run's result</p></figcaption></figure>


# Options

Add user-facing configuration to your Mechanic tasks — input fields, validation flags, and custom option types.

[**Tasks**](/core/tasks) can accept user configuration via **options**. Options are created automatically: when you reference `options.something` in your task's [**code**](/core/tasks/code), Mechanic adds an input field for it in the task configuration UI.

You can add **flags** to an option key (like `__required` or `__select_...`) to control what kind of input is shown and how it's validated.

Mechanic flags provide only limited option validation. For anything more advanced, a task can define [**custom validation**](/core/tasks/options/custom-validation).

## Quick start

Paste any of these into your task code (even inside a `{% comment %}` block) to create an option:

Options flagged with `__userform` will also appear on the **Run task** form for tasks that subscribe to `mechanic/user/form`, and on the Run tasks page for Shopify admin action links (mechanic/user/{resource} topics). These are [User Form](/core/tasks/user-form) fields, and submitted values are available in Liquid as `input.<name>`. See [Shopify admin action links](/core/shopify/admin-action-links) for the end-user flow.

```liquid
{{ options.note }}
{{ options.subject__required }}
{{ options.body__multiline }}
{{ options.reply_to__email_required }}
{{ options.channels__multiselect_o1_email_o2_sms }}
{{ options.enabled__boolean }}
{{ options.count__number }}
{{ options.tags__array }}
{{ options.shared_username__global_required }}
{{ options.api_token__secret_required }}
{{ options.mode__select_o1_test_o2_live }}
{{ options.run_mode__select_o1_test_o2_live__userform__required }}
```

## 1. Keys

Options are available in the `options` Liquid variable. Each option key becomes one field in the UI, and one value at runtime.

Option keys must contain only lowercase letters, numbers, and underscores. Mechanic uses the key to generate a label (underscores become spaces).

```
<name>[__<flag>[ _<flag> ... ]][__<flag>[ _<flag> ... ] ...]
```

| Part   | Rules                                                    | Example                              |
| ------ | -------------------------------------------------------- | ------------------------------------ |
| `name` | Lowercase letters, numbers, and underscores only.        | `send_after`                         |
| `flag` | One or more tokens that customise the field (see below). | `required`, `date`, `picker_product` |

Flags are usually specified after the first `__`, separated with underscores (`_`). For complex option types (notably `select` / `choice` / `multiselect`), you may also append additional flag segments using `__` (e.g. `__userform__required`) to avoid accidentally changing the last choice value.

Mechanic finds options by scanning your task code, so each option must be referenced at least once using a normal lookup like `options.foobar`.

***

## 2. Display Order

Options are displayed to the user in the order in which they are first referenced in the task code.

Because this may not result in a natural sequence, it can be useful to prefix task code with a comment block, explicitly referencing each option so as to force the overall order.

```liquid
{% comment %}
  Option order:

  {{ options.api_key__required }}
  {{ options.mode__select_o1_test_o2_live }}
  {{ options.webhooks__array }}
{% endcomment %}
```

***

## 3. Flags

Option flags control how an option appears and behaves in a task's configuration form, and also control the type and format of the option value.

Many flags may be combined with other flags, for more nuanced control.

If no flags are used for an option, an option will be made available as a plain text field, and the option value will be a string.

The special **`userform`** flag does not change the input type or validation; it simply marks an option as one that should appear on the **Run task** form (topic `mechanic/user/form`) and the Run tasks page for Shopify admin action links, in addition to the general task options screen. See [User Form](/core/tasks/user-form) for details.

#### Flags fall into three categories:

| Category            | Purpose                                                                                                    |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Input types**     | Choose what kind of input you want (date picker, slider, select list, …). Pick exactly **one** input-type. |
| **Form modifiers**  | Fine‑tune how the control validates or behaves (required, email, etc.).                                    |
| **Auxiliary flags** | Extra behaviour for certain input types (future‑only dates, multi‑select choices, etc.).                   |

### 3.1 Input‑type flags

<table><thead><tr><th>Flag</th><th>UI control</th><th width="197.44140625">Value returned</th><th>Key examples</th></tr></thead><tbody><tr><td><em>(none - default)</em></td><td>Single‑line text</td><td><code>string</code></td><td><code>options.subject</code></td></tr><tr><td><code>multiline</code></td><td>Multiline text box</td><td><code>string</code></td><td><code>options.body__multiline</code></td></tr><tr><td><code>boolean</code></td><td>Checkbox</td><td><code>true/false</code></td><td><code>options.enabled__boolean</code></td></tr><tr><td><code>number</code></td><td>Numeric input (<code>step=1</code>)</td><td><code>number</code></td><td><code>options.count__number</code></td></tr><tr><td><code>code</code></td><td>Code‑formatted text area</td><td><code>string</code></td><td><code>options.script_snippet__code_multiline</code></td></tr><tr><td><code>keyval</code></td><td>Key → value repeater</td><td><code>hash</code></td><td><code>options.headers__keyval</code></td></tr><tr><td><code>array</code></td><td>Value repeater</td><td><code>array</code></td><td><code>options.tags__array</code></td></tr><tr><td><code>date</code></td><td>Calendar picker</td><td><code>"YYYY‑MM‑DD"</code></td><td><code>options.launch_date__date</code></td></tr><tr><td><code>datetime</code></td><td>Date+time picker</td><td><code>"YYYY‑MM‑DDTHH:MM"</code></td><td><code>options.send_at__datetime</code></td></tr><tr><td><code>time</code></td><td>Time‑only picker</td><td><code>"HH:MM"</code></td><td><code>options.quiet_time__time</code></td></tr><tr><td><code>color</code></td><td>Hex colour picker</td><td><code>"#RRGGBB"</code></td><td><code>options.theme_color__color</code></td></tr><tr><td><code>range_minX_maxY_stepZ</code></td><td>Slider + number box</td><td><code>number</code></td><td><code>options.qty__range_min0_max100_step5</code></td></tr><tr><td><code>select</code></td><td>Single‑choice dropdown</td><td><code>string</code></td><td><code>options.plan__select_o1_basic_o2_pro</code></td></tr><tr><td><code>choice</code></td><td>Radio buttons</td><td><code>string</code></td><td><code>options.tier__choice_o1_gold_o2_silver</code></td></tr><tr><td><code>multiselect</code></td><td>Checkbox list</td><td><code>array[string]</code></td><td><code>options.channels__multiselect_o1_email_o2_sms</code></td></tr><tr><td><code>picker_&#x3C;resource></code></td><td>Shopify resource picker</td><td><code>gid string</code></td><td><code>options.product__picker_product</code></td></tr><tr><td><code>picker_&#x3C;resource>_array</code></td><td>Multi‑select resource picker</td><td><code>array[gid]</code></td><td><code>options.products__picker_product_array</code></td></tr></tbody></table>

Shop configuration input types are also available:

| Flag     | UI control               | Value returned          | Key examples                               |
| -------- | ------------------------ | ----------------------- | ------------------------------------------ |
| `global` | Dropdown of shop globals | selected global value   | `options.shared_username__global_required` |
| `secret` | Dropdown of shop secrets | opaque secret reference | `options.api_token__secret_required`       |

See [Globals and secrets](/platform/globals-and-secrets) for setup, the supported actions and filters, and safety details.

When an option key uses a structured input type, that type wins before `global` or `secret` are considered. For example, `options.mode__select_o1_global_o2_secret_o3_local` is still a select dropdown whose choices are the literal strings `"global"`, `"secret"`, and `"local"`.

{% hint style="info" %}
Array options have a hidden feature: once the user-configured array reaches 5 elements in size, a new "Manage in bulk" button will appear for that option. Clicking it will open a modal which allows the user to manage the array's input using a single multiline text field, in which each line represents an array element. This is a convenient way to configure larger arrays.
{% endhint %}

#### Choice/Select grammar

Use ordinal tokens to define values:

```
<name>__select_o1_basic_o2_pro_o3_enterprise
               │  │     │  │
               │  │     │  └───── value #2
               │  │     └──────── ordinal marker
               │  └──────────── value #1
               └─────────────── ordinal marker
```

Ordinals (`o1`, `o2`, …) determine display order. Underscores inside values are preserved.

Because flags are parsed from underscore-separated tokens, avoid using flag words like `required` and `userform` as literal choice values.

**Combining choice/select with `userform` / `required`**

For `select`, `choice`, and `multiselect`, prefer appending modifier flags as additional `__` segments:

* `options.aggregate__select_o1_first_o2_last_o3_sum__userform__required`

Appending modifiers at the end of the same underscore-delimited segment also works:

* `options.aggregate__select_o1_first_o2_last_o3_sum_userform_required`

Do not put modifiers before the input-type flag (e.g. `options.aggregate__userform_select_...` or `options.aggregate__required_select_...`).

#### Range grammar

* `min<number>` – **required**
* `max<number>` – **required**
* `step<number>` – optional (defaults to `1`).

#### Picker grammar

```
<name>__picker_<product|variant|collection>[_array]
```

* Append `_array` for multi‑select.
* Unsupported resources are rejected during validation.

### 3.2 Form‑modifier flags

| Flag       | Applies to | Effect                                                                                               |
| ---------- | ---------- | ---------------------------------------------------------------------------------------------------- |
| `required` | Any        | Field must be filled before Save.                                                                    |
| `email`    | `text`     | Adds email placeholder and basic email format check.                                                 |
| `userform` | Any        | Shows this option on **Run task** form (`mechanic/user/form`) and on Shopify admin action link runs. |

### 3.3 Auxiliary flags

| Flag         | Works with         | Effect                       |
| ------------ | ------------------ | ---------------------------- |
| `futureonly` | `date`, `datetime` | Picker disallows past dates. |

***

## 4. Built‑in validation

1. **Required fields** must not be empty.
2. **Range sliders** need both `min` and `max`.
3. **Pickers** only allow `product`, `variant`, or `collection`.
4. **Email** inputs are matched against a basic regex.
5. **Secret** options cannot be combined with `userform`.

Custom rules? Learn more about [custom validation](/core/tasks/options/custom-validation).

***

## 5. Liquid

Options that allow text input are evaluated for Liquid when a task processes an event. Liquid evaluation for options occurs before it occurs for task code, which means that any Liquid variables created by task code are not available to task options.

Liquid code in task options have access to the same set of [environment variables](/core/tasks/code/environment-variables) that are made available to the task code, including `event`, `shop`, `cache`, and any event subject variables.

***

## 6. Quick reference

| Goal                  | Key snippet                                                | Value example                  |
| --------------------- | ---------------------------------------------------------- | ------------------------------ |
| Text input            | `options.subject`                                          | `"Welcome!"`                   |
| Text input (required) | `options.subject__required`                                | `"Welcome!"`                   |
| Email input           | `options.reply_to__email_required`                         | `"person@example.com"`         |
| Checkbox              | `options.newsletter__boolean`                              | `false`                        |
| Key–value map         | `options.headers__keyval`                                  | `{ "X-Env": "staging" }`       |
| String list           | `options.tags__array`                                      | `["vip","wholesale"]`          |
| Global selector       | `options.shared_username__global_required`                 | `"matt"`                       |
| Secret selector       | `options.api_token__secret_required`                       | secret reference               |
| 0–100 slider          | `options.score__range_min0_max100`                         | `42`                           |
| Colour picker         | `options.bg__color`                                        | `"#336699"`                    |
| Dropdown              | `options.plan__select_o1_basic_o2_pro`                     | `"basic"`                      |
| Run task dropdown     | `options.plan__select_o1_basic_o2_pro__userform__required` | `"basic"`                      |
| Multi‑select          | `options.channels__multiselect_o1_email_o2_sms`            | `["email","sms"]`              |
| Product picker        | `options.promo__picker_product`                            | `"gid://shopify/Product/123"`  |
| Product list          | `options.products__picker_product_array`                   | `[ "gid://…/1", "gid://…/2" ]` |
| Date                  | `options.go_live__date_required`                           | `"2025-05-06"`                 |
| Time                  | `options.quiet_at__time`                                   | `"00:25"`                      |
| Datetime              | `options.party__datetime`                                  | `"2031-04-22T15:13:00"`        |

### Working with date options

Formatting date/time option values

```
{{ options.launch_date__date | date: "%Y-%m-%d" }}
   ⇒ 2025-05-06

{{ options.quiet_at__time  | date: "%H:%M" }}
   ⇒ 00:25

{{ options.party__datetime     | date: "%Y-%m-%d %H:%M" }}
   ⇒ 2031-04-22 15:13
```

#### Converting to another timezone

```
{{ options.party__datetime | date: tz: "UTC" }}
   ⇒ 2031-04-22T19:13:00Z       {offset shifted +4 h}

{{ options.quiet_at__time | date: "%H:%M %Z", tz: "America/Vancouver" }}
   ⇒ 21:25 PDT
```


# Custom validation

A task may enforce **custom validation** for options by including validation logic in its code, inspecting the current value of an option and rendering an [**error object**](/core/tasks/code/error-objects) if the option does not meet its criteria.

A modification to a task option will always result in a new [**preview**](/core/tasks/previews) being rendered. In this way, a task developer may provide the user with immediate feedback on their task configuration.

## Example

In this example, a task begins by validating an option called "A positive number". The only flags on this option are "required" and "number", meaning that Mechanic's involvement is limited to making sure the user fills in this task option with a number.

Once the option is filled in, the task preview will be rendered. If the user has entered a zero, or a negative number, the [error tag](/platform/liquid/tags/error) is used to generate an [error object](/core/tasks/code/error-objects). The error message will then be shown to the user, and they will be prevented from saving the task until they provide valid input.

```liquid
{% if options.a_positive_number__required_number <= 0 %}
  {% error "The option 'A positive number' must be greater than zero." %}
{% endif %}

{% action "cache", "set", "a_positive_number_to_remember", options.a_positive_number__required_number %}
```


# Previews

Test Mechanic tasks safely before they run on real data — preview actions, request Shopify permissions, and define sample events.

A task uses its **preview** to demonstrate what actions the task intends to generate. Among other purposes (see below), this is also how tasks request the Shopify permissions they require.

Mechanic generates a task preview by rendering the task code using a **preview event**, which resembles a live event that the task may see. The task is then responsible for rendering **preview actions** in response to the preview event, actions which are visually presented to the user and are analyzed by the platform, but are never actually performed.

If you are editing tasks locally, the [Mechanic CLI](/resources/mechanic-cli) can preview a local task file without saving it to Mechanic.

{% hint style="info" %}
**Task previews cannot access the Mechanic cache or the Shopify Admin API.** This restriction is made to increase the predictability and performance of task previews.

To provide tasks with relevant sample data during preview, developers can [define preview events](/core/tasks/previews/events) (to construct relevant scenarios at the event level) or use [stub data](/core/tasks/previews/stub-data) (to swap in predefined values for [environment variables](/core/tasks/code/environment-variables), or for the results of data that would otherwise come from the Mechanic cache or the Shopify Admin API).
{% endhint %}

## Purposes

A preview has three critical purposes:

1. Showing the user that the task will do what they expect it to do
2. Showing the task developer that the task code is functioning as intended
3. Showing the Mechanic platform what permissions the task requires

### For users

Core to the design of Mechanic is the idea that we can make it easy to make it easy – in this case, making it easy for developers to show their users what a Mechanic task can be expected to do.

By rendering preview actions, a task can prove to the user that it is interpreting their configuration as they intended. For example, by rendering a preview [Email](/core/actions/email) action, a task can show the user that their configured email content is appearing as expected inside the email body. This increases trust in the task, and allows users confidence in the task's outcome, even before the task processes a live event.

### For developers

Developers may think of previews as a sort of test, using preview actions to prove that their task code is functioning as intended. A quality task will exercise all of its code in response to a preview event; doing so gives the developer instant feedback on task results, without actually having to run the task with a live event.

### For Mechanic

At the platform level, Mechanic uses previews to determine what permissions a task requires.

Mechanic gets this information from the actions that a task generates during preview, as well as from analysis of the Liquid lookups and GraphQL queries that a task uses during runtime.

For example, if a task renders a [Shopify](/core/actions/shopify) action containing a [customerCreate](https://shopify.dev/api/admin-graphql/latest/mutations/customercreate) mutation, Mechanic will prompt the user to grant access to the `write_customers` Shopify OAuth scope. If Mechanic observes a task using `shop.customers`, or observes the [shopify](/platform/liquid/filters#shopify) filter receiving a customer-related GraphQL query, it will prompt for the `read_customers` scope.

{% hint style="warning" %}
Some GraphQL mutations have multiple potential scope requirements, like [tagsAdd](https://shopify.dev/api/admin-graphql/latest/mutations/tagsadd) or [metafieldsSet](https://shopify.dev/api/admin-graphql/latest/mutations/metafieldsset). Because the requirements of these mutations hinge on their arguments, make sure that your preview actions are rendered with realistic ID strings (e.g. `id: "gid://shopify/Product/12345"`). Mechanic will look for these IDs to determine what scopes to request.
{% endhint %}

## Sources

Previews are generated using synthetic, temporary, non-persisted events – at least one for each event topic that the task subscribes to. These events are sourced from one of three places, in order of priority:

1. If the task [defines its own preview event](/core/tasks/previews/events) for a given topic, the preview will use the defined event;
2. Or, if the Mechanic account has a recent event with a matching topic on file, the preview will use data from that event;
3. Or, if the event topic is standard and known to Mechanic (i.e. not a part of [the User domain](/platform/events)), the preview will use illustrative example event data defined by the Mechanic platform.

## Detecting preview events

A preview event is identical to a live event in all respects but one: it contains a `preview` attribute, set to `true`, identifying it as a preview event.

{% hint style="warning" %}
For live events, the `preview` attribute does not exist. This means that `event.preview == false` is not a valid way to detect a live event. Instead, use `event.preview != true`, or `event.preview == nil`.
{% endhint %}

{% tabs %}
{% tab title="Task code" %}

```liquid
{% if event.preview %}
  {% log "This is a preview event, generated by Mechanic." %}
{% else %}
  {% log "This is a live event, received by Shopify." %}
{% endif %}

{% if event.preview != true %}
  {% log "This is a live event, received by Shopify." %}
{% else %}
  {% log "This is a preview event, generated by Mechanic." %}
{% endif %}
```

{% endtab %}
{% endtabs %}

A preview event's data is taken from the Mechanic account's event history, providing a realistic sample of the data a task can expect to see. (If the account history has no events for a given topic just yet, Mechanic will attempt to use anonymous sample event data of its own.)

## Rendering preview actions

A developer can choose between rendering static and dynamic preview actions. Static preview actions are hard-coded, written to appear whenever `event.preview` is true. Dynamic preview actions are the result of the task code running normally, using event data in preview in the same way that it would use that event data with a live event. Because dynamic preview actions are the result of meaningfully exercising the task's code, they can provide a good indicator of how the task will behave with a live event. By contrast, static preview actions do not provide useful feedback on how a task is coded.

### Static preview actions

A static preview action is rendered in direct response to `event.preview`. In general, it's better to use [dynamic preview actions](#dynamic-preview-actions), but an understanding of both techniques is useful.

In the following example, a static preview action demonstrates that the task intends to tag incoming orders with "web". In actuality, the task's intent is to only tag orders that arrive via the Online Store channel; because the task can't be sure whether or not the preview event will contain such an order, a static preview action is used to ensure that a preview event always results in a tagging action.

Branching a task like this has two problems:

1. The actual condition of the task is not exercised during preview. The task will need to be tested with live orders from multiple channels, in order to verify that the task works properly.
2. Duplicating code makes it easier for one copy of the code to fall out of date. By using completely different code for the preview and live actions, it becomes easier for developers to forget to keep the two copies in sync as the task evolves.

```liquid
{% if event.preview %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: "gid://shopify/Order/1234567890", tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% elsif order.source_name == "web" %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ order.admin_graphql_api_id | json }}, tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% endif %}
```

**Subscriptions**

```
shopify/orders/create
```

### Dynamic preview actions

A dynamic preview action is the natural result of exercising a task's code as completely as possible, without adding any business logic that responds to `event.preview`. Put another way, the idea is to make the preview (that appears during task editing) look as similar to a live event as possible.

{% code title="Example" %}

```liquid
{% if some_evaluated_condition %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ order.admin_graphql_api_id | json }}, tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% endif %}
```

{% endcode %}

There are two techniques available for "steering" the task towards desired outcomes during preview.

1. Use [**defined preview events**](/core/tasks/previews/events) to control preview event data, without ever having to add preview-related code to the task itself. This is the cleanest way to control data provided by the event during preview.
2. Use [**stub data**](/core/tasks/previews/stub-data) to dynamically swap in preview-friendly values. This is generally not necessary for preview *event* data, but may be necessary when querying Shopify for data during a task: because the Shopify API is disabled during preview, using stub data can be useful for swapping in realistic values that *would* be returned during a live run.


# Defining preview events

During [**task preview**](/core/tasks/previews), Mechanic scans the task's [**subscriptions**](/core/tasks/subscriptions). For each [**event topic**](/core/events/topics) found, Mechanic constructs a synthetic **preview event**, resembling one that the task might encounter during live use.

By default, each preview event's data is sampled from previous events that the Mechanic account has seen, for the same topic.

However, developers may define their own preview events, containing whatever data the developer wishes to use for preview. This may be useful for several reasons:

* Most tasks conditionally respond to events based on their data. Controlling the event data present during preview allows the developer to deterministically verify the results of the action.
* Further, by deterministically/predictably generating actions, the developer can consistently demonstrate the permissions they need to Mechanic. (To learn more about this, see [Previews](/core/tasks/previews).)
* Defining preview event data is usually simpler than defining [stub data](/core/tasks/previews/stub-data).
  * Stubbing the `event` variable (or any of the [subject variables](/core/tasks/code/environment-variables#event-subject-variables)) removes any intelligence from the objects Mechanic generates from event data, a drawback avoided by defining a preview event and its data. Using the [Order object](/platform/liquid/objects/shopify/order) as an example, a task may typically access its custom attributes via `order.note_attributes.color`, or via `order.note_attributes[0].value`. This dynamic behavior is lost if the `event` variable is stubbed out, which can result in behaviors that are difficult to diagnose.
* Multiple preview events may be defined per event topic. This allows developers to verify that their task renders the appropriate results under a variety of circumstances.
  * Defined preview events can be labeled with a description, which is visible in the task preview pane. This makes it easy to identify the scenario that a preview event is meant to represent.

{% hint style="info" %}
Preview event definitions cannot provide for return values from Shopify query operations (i.e. output from the [shopify filter](/platform/liquid/filters#shopify), or the result of traversing Shopify Liquid objects, as in `customer.orders.first`). For those purposes, use the [**stub data**](/core/tasks/previews/stub-data) technique.
{% endhint %}

## Configuration

Preview events may be defined using the "Edit preview events" button, in the task preview pane.

The configuration area for preview events contains a quickstart link for each event topic the task subscribes to, allowing developers to get started using sample data if the event topic is known to Mechanic. Or, the developer may start with a blank preview event definition, filling in whatever topic and data are useful.

A developer may define any number of preview events per topic. If no preview events are defined for a given topic, Mechanic will construct its own ad-hoc event during preview.

### Properties

#### Description

Displayed beneath the event topic in the preview pane, allowing the developer to distinguish one scenario from another.

#### Topic

Identifies the event definition to Mechanic, when Mechanic goes to construct preview events by topic.

#### Data

Used to construct `event.data`, and may be set to whatever values are useful in representing a specific scenario. The data structures used here should resemble what Mechanic will receive for a live event of the same topic.

Notably, the data here *can* be limited to just the properties that are useful. For example, while Mechanic might normally generate a complete payload for shopify/orders/create, the developer might only care about the `"email"` property of the order – and so their defined preview event data might be limited to just that property. (Note that the inverse may not be true: defining preview event data for traversals into other objects, e.g. using preview event data to define a value for `order.line_items[0].product.title`, will *not* work.)

### Example

For a trivial task, subscribing to shopify/customers/create, and having the following task code...

```liquid
{% if customer.email contains "gmail.com" %}
  {% log message: "got a gmail user!", email: customer.email %}
{% else %}
  {% log message: "got someone else!", email: customer.email %}
{% endif %}
```

... we define two preview events, one which represents a Gmail user, and one which does not. This allows us to easily assert that the task behaves properly in both scenarios.

![](/files/M7niMe7VYgRXQ8kHaUwi)

## Versioning

Preview event definitions are stored along with the task itself, and thus are present in the tasks version history (and, naturally, in task exports).

Because definitions are a part of the task itself, they're appropriate for use as a testing tool, allowing the developer to verify that a task behaves as intended at every stage of the task's development.


# Stub data

**Stub data** is hard-coded into a task, providing an unchanging source of data for [**previews**](/core/tasks/previews). It is an important tool when generating [**dynamic preview actions**](/core/tasks/previews#dynamic-preview-actions). Stub data may be used for user-defined variables, but may also override [**environment variables**](/core/tasks/code/environment-variables) as needed.

{% hint style="info" %}
For controlling preview event data (i.e. the values in `event.data`, and values found in [event subject variables](/core/tasks/code/environment-variables#event-subject-variables)), use [**defined preview events**](/core/tasks/previews/events) to cleanly specify these values *outside* of the task code.
{% endhint %}

## Stubbing Liquid variables

Most tasks make decisions based on the [Liquid variables](/core/tasks/code/environment-variables) automatically provided, making it a common practice to stub them during preview mode. Any and all Liquid variables may be replaced by stub data, including `event` and any [event subject variables](/core/tasks/code/environment-variables#event-subject-variables).

In simple cases, replacement objects may be constructed using the [assign](/platform/liquid/tags/assign) tag.

{% hint style="info" %}
The stub data in the following examples include an ID for the order, so as to generate a realistic tagsAdd mutation during preview mode.

Realistic preview actions are important for users and developers, but there's a functional importance for tagsAdd mutations in particular: in preview mode, Mechanic looks at the `id` argument in order to determine what kind of resource will be tagged, in order to determine what permissions this particular mutation requires. If you generate tagsAdd mutations during preview, make sure to use realistic ID values!
{% endhint %}

```liquid
{% if event.preview %}
  {% assign order = hash %}
  {% assign order["source_name"] = "web" %}
  {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
{% endif %}

{% if order.source_name == "web" %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ order.admin_graphql_api_id | json }}, tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% endif %}
```

It's also possible to construct this data using [parse\_json](/platform/liquid/filters#json-parse_json-parse_jsonl).

```liquid
{% if event.preview %}
  {% capture order_json %}
    {
      "source_name": "web",
      "admin_graphql_api_id": "gid://shopify/Order/1234567890"
    }
  {% endcapture %}

  {% assign order = order_json | parse_json %}
{% endif %}

{% if order.source_name == "web" %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ order.admin_graphql_api_id | json }}, tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% endif %}
```

## Stubbing GraphQL data

Mechanic makes GraphQL data available to tasks via the [shopify](/platform/liquid/filters#shopify) filter. Mechanic observes the shopify filter in action during preview mode, using its inputs to inform Mechanic's knowledge of what permissions the task needs.

For this reason, it's important to allow the shopify filter to run normally, and construct stub data afterwards.

It can be useful to specify stub data using JSON, fed through the [parse\_json](/platform/liquid/filters#json-parse_json-parse_jsonl) filter. Sample JSON is easy to generate using [Shopify's GraphiQL app](https://shopify-graphiql-app.shopifycloud.com/).

{% tabs %}
{% tab title="GraphQL with stub data" %}

```liquid
{% capture query %}
  query {
    publications(first: 250) {
      edges {
        node {
          id
          name
        }
      }
    }
  }
{% endcapture %}

{% assign result = query | shopify %}

{% if event.preview %}
  {% capture result_json %}
    {
      "data": {
        "publications": {
          "edges": [
            {
              "node": {
                "id": "gid://shopify/Publication/69217648807",
                "name": "Online Store"
              }
            }
          ]
        }
      }
    }
  {% endcapture %}

  {% assign result = result_json | parse_json %}
{% endif %}

{% log available_publications: result.data.publications %}
```

{% endtab %}

{% tab title="GraphQL pagination with stub data" %}

```liquid
{% assign cursor = nil %}
{% assign total_inventory = 0 %}

{% for n in (0..100) %}
  {% capture query %}
    query {
      orders(
        first: 250
        query: "status:open"
        after: {{ cursor | json }}
      ) {
        pageInfo { hasNextPage }
        edges {
          node { name, email }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "orders": {
            "pageInfo": {
              "hasNextPage": false
            },
            "edges": [
              {
                "node": {
                  "name": "#1135",
                  "email": "isaac@example.com"
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% for order_edge in result.data.orders.edges %}
    {% assign order_node = order_edge.node %}

    {% if order_node.email == blank %}
      {% continue %}
    {% endif %}

    {% action "email" %}
      {
        "to": {{ order_node.email | json }},
        "subject": {{ "We're still working on " | append: order_node.name | json }},
        "body": "Thanks for your patience!"
      }
    {% endaction %}
  {% endfor %}

  {% if result.data.orders.pageInfo.hasNextPage %}
    {% assign cursor = result.data.orders.edges.last.cursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}
```

{% endtab %}
{% endtabs %}


# Shopify API version

Each task is configured with a specific [**Shopify API version**](/core/shopify/api-versions), defaulting to the latest version at the time of the task's creation.

This version is used in all activity related to the current task, including:

* REST API calls performed to support Liquid lookups
* GraphQL calls performed by [the shopify Liquid filter](/core/tasks/shopify-api-version)
* All Shopify API calls performed by [the Shopify action](/core/actions/shopify), including [bulk operations](/core/tasks/shopify-api-version)

{% hint style="info" %}
When a task run starts, it checks the Shopify API version configured for the task at that time. Action runs always inherit their Shopify API version from their task run. This means that changing a task's Shopify API version can affect queued task runs, but won't change queued action runs.
{% endhint %}

## Using "unstable"

All Shopify API versions are named with a specific date (i.e. "2021-07"), except for "unstable". This version receives regular updates from Shopify, and its features may change without notice.

Most tasks should use a dated version, to maximize the amount of time a task can rely on a specific set of Shopify API features.

## Automatic version upgrades

Shopify supports each version for 12 months (except for "unstable", which is always available). 30 days before a task's version becomes unsupported, Mechanic will automatically begin calling the closest supported version instead.

Shopify may, at times, mark certain API features as deprecated. If a Mechanic account calls a deprecated API, Mechanic will display the deprecation notice in the app. Learn more about [Shopify API deprecations](/core/shopify/api-versions#deprecations).

## Listing versions

For local task development, `mechanic tasks list` shows each task's configured Shopify API version. The [task sync API](/resources/mechanic-cli/mechanic-task-sync-api) also includes `shopify_api_version` in task sync envelopes, for trusted scripts and automation.

## Changing versions

The selector for a task's Shopify API version is available in Advanced mode, below the task code area.

![](/files/K19JmHMyXgaafZhSItsC)


# Permissions

How Mechanic determines Shopify API access scopes from your task code — automatic detection and explicit declarations.

Mechanic determines Shopify API access scopes (like `read_orders` or `write_products`) from your task code. It can infer scopes from several places, including subscriptions, Shopify data access in task code, and actions rendered during preview. When your shop needs additional access, Mechanic will surface that in the app. The Permissions area in Settings shows granted and required scopes; it does not grant scopes directly.

There are two approaches, and both are fully supported:

## Automatic detection

By default, Mechanic infers the permissions a task needs automatically.

This can happen through:

* Shopify event subscriptions
* Shopify data your task reads in Liquid
* Shopify actions your task generates during [preview](/core/tasks/previews)
* Active [custom Shopify webhooks](/platform/shopify/custom-webhooks) — when an enabled task subscribes to a webhook's `user/...` topic, Mechanic can include that webhook's Shopify topic when determining required scopes.

A [preview](/core/tasks/previews) is Mechanic's safe, non-destructive rendering of your task. Previews are valuable for more than permissions: they show users what the task will do, and they give developers a safe way to verify task behavior. They are still especially important when permission requirements depend on action arguments. For example, mutations like `tagsAdd` or `metafieldsSet` may require different scopes depending on the resource ID used, so realistic preview data still matters.

To get the best results:

* Make sure your preview actions include realistic resource IDs (e.g. `gid://shopify/Product/12345`), especially for mutations like `tagsAdd` or `metafieldsSet` that have multiple potential scope requirements.
* Use [defined preview events](/core/tasks/previews/events) and [stub data](/core/tasks/previews/stub-data) to provide your task with realistic sample data during preview.

For full details on how previews work, see [Previews](/core/tasks/previews).

## Explicit declaration with `{% permissions %}`

You can declare the exact scopes your task needs directly in your task code using the `{% permissions %}` tag:

```liquid
{% permissions %}
read_orders
write_products
{% endpermissions %}
```

This tells Mechanic precisely which scopes to request, without relying on automatic detection. It's simpler and more predictable — useful when:

* Your task's permissions are hard to infer automatically
* You want to be explicit about what your task needs
* Your task uses scopes that aren't easily demonstrated during preview

For full syntax details, see the [`permissions` tag reference](/platform/liquid/tags/permissions).

## When to use which

**Automatic detection** works well for many tasks out of the box, especially when the task's subscriptions, Shopify data access, and previewed actions clearly demonstrate its intent.

**Explicit declaration** is simpler when permissions are hard to infer, when Mechanic fails to detect a permission your task needs, or when setting up realistic previews would be more work than just listing the scopes directly.

Both approaches are valid, and you can choose whichever fits your task best.

## Troubleshooting

If your task is failing because of a missing permission, see [My task is failing because of a permissions problem](/faq/my-task-is-failing-because-of-a-permissions-problem) for debugging steps. For general questions about managing Shopify scopes, see [How do I add a Shopify access scope to my task?](/faq/how-do-i-add-a-shopify-access-scope-to-my-task)


# Advanced settings

Per-task configuration beyond code and options — documentation, JavaScript, and action sequencing.

Advanced settings are per-task configuration options beyond the task's code and user-facing options. They control how the task is documented, how its code is executed, and how its actions are performed.

* [Documentation](/core/tasks/advanced-settings/documentation) — add Markdown documentation that is displayed to users alongside the task
* [JavaScript](/core/tasks/advanced-settings/javascript) — attach JavaScript to the online storefront or order status page
* [Perform action runs in sequence](/core/tasks/advanced-settings/perform-action-runs-in-sequence) — force the task's actions to run one at a time, in order, instead of concurrently


# Documentation

Individual tasks may be optionally configured with their own **documentation**, formatted with Markdown, providing the user with anything they should know about the task's usage and operation.

Task documentation is shown to the user below the task's [**options**](/core/tasks/options). Documentation is also shown in the confirmation prompt a user sees when they trigger a task that subscribes to either mechanic/user/trigger or mechanic/user/text.

## Adding documentation

Task documentation may be managed in Advanced mode.

![](/files/atPQKBMTa24aHNMJysvs)

In Basic mode, documentation is displayed below the task options.

![](/files/aXqX3YC5SJhCmCvtHPV8)

## Markdown

Task documentation may be formatted with Markdown, a common syntax for text formatting.

The following table comes from [markdownguide.org/cheat-sheet](https://www.markdownguide.org/cheat-sheet/), and is presented with no changes, under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) license.

| **Element**                                                                     | **Markdown Syntax**                          |
| ------------------------------------------------------------------------------- | -------------------------------------------- |
| [Heading](https://www.markdownguide.org/basic-syntax/#headings)                 | `# H1 ## H2 ### H3`                          |
| [Bold](https://www.markdownguide.org/basic-syntax/#bold)                        | `**bold text**`                              |
| [Italic](https://www.markdownguide.org/basic-syntax/#italic)                    | `_italicized text_`                          |
| [Blockquote](https://www.markdownguide.org/basic-syntax/#blockquotes-1)         | `> blockquote`                               |
| [Unordered List](https://www.markdownguide.org/basic-syntax/#unordered-lists)   | `- First item - Second item - Third item`    |
| [Ordered List](https://www.markdownguide.org/basic-syntax/#ordered-lists)       | `1. First item 2. Second item 3. Third item` |
| [Code](https://www.markdownguide.org/basic-syntax/#code)                        | `code`                                       |
| [Horizontal Rule](https://www.markdownguide.org/basic-syntax/#horizontal-rules) | `---`                                        |
| [Link](https://www.markdownguide.org/basic-syntax/#links)                       | `[title](https://www.example.com)`           |
| [Image](https://www.markdownguide.org/basic-syntax/#images-1)                   | `![alt text](image.jpg)`                     |

## Keyboard Shortcuts

The Markdown editor in Mechanic includes keyboard shortcuts for common formatting operations. These shortcuts can also be accessed through the **right click menu** in the editor.

<details>

<summary>🎬 Watch: Keyboard Shortcuts Demo</summary>

<figure><img src="/files/82duTco51Nm7P5gNfKgJ" alt=""><figcaption></figcaption></figure>

</details>

<table data-header-hidden><thead><tr><th width="134.640625">Action</th><th width="208.4453125">Shortcut</th><th>Description</th></tr></thead><tbody><tr><td><strong>Action</strong></td><td><strong>Shortcut</strong></td><td><strong>Description</strong></td></tr><tr><td><strong>Toggle Bold</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>B</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>B</kbd></td><td><p>Adds or removes <strong>Bold</strong> formatting on selected text or current word.</p><pre><code>**text**
</code></pre></td></tr><tr><td><strong>Toggle Italic</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>I</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>I</kbd></td><td><p>Adds or removes <em>Italic</em> formatting on selected text or current word.</p><pre><code>_text_
</code></pre></td></tr><tr><td><strong>Toggle Strikethrough</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>X</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>X</kbd></td><td><p>Adds or removes <del>Strikethrough</del> on selected text or current word.</p><pre><code>~text~
</code></pre></td></tr><tr><td><strong>Toggle Heading</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd></td><td><p>Cycles through <strong>Heading</strong> formats.</p><pre><code><strong>→ H1     → H2      → H3       → Plain Text
</strong>→ # text → ## text → ### text → text 
</code></pre></td></tr><tr><td><strong>Toggle Inline Code</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd></td><td><p>Adds or removes <code>Inline Code</code> formatting on selected text or current word.</p><pre><code>`code`
</code></pre></td></tr><tr><td><strong>Toggle Code Block</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd></td><td><p>Adds or removes a <code>Code Block</code> on multiline selection or blank line.</p><pre data-full-width="false"><code><strong>```
</strong>code
```
</code></pre></td></tr><tr><td><strong>Toggle List</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd></td><td><p>Cycles through <strong>List</strong> formats.</p><pre><code><strong>→ Unordered List → Ordered List → Plain Text
</strong>→ - text         → 1. text      → text
</code></pre></td></tr><tr><td><strong>Toggle Link</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd></td><td><p>Adds or removes <strong>Link</strong> formatting.</p><pre><code>[text](url)
</code></pre></td></tr><tr><td><strong>Toggle Blockquote</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd></td><td><p>Adds or removes <strong>Blockquote</strong> formatting.</p><pre><code>> text 
</code></pre></td></tr><tr><td><strong>Toggle Image</strong></td><td><i class="fa-apple">:apple:</i>: <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd><br><i class="fa-windows">:windows:</i>: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd></td><td><p>Adds or removes <strong>Image</strong> formatting.</p><pre><code>![](url)
</code></pre></td></tr></tbody></table>

### **Pasting URLs**

When pasting a valid URL, if you have text selected the selected text will automatically be wrapped in Markdown link syntax using the pasted URL. The selected text becomes the link text, and the pasted URL becomes the link destination.

```
→ [selected text](https://pastedurl.com)
```


# JavaScript

Shopify allows apps to inject JavaScript into the online storefront. (This is facilitated by [ScriptTag](https://shopify.dev/docs/admin-api/rest/reference/online-store/scripttag) in the Shopify API.)

Mechanic supports this by allowing each task to specify its own JavaScript, to be injected into the online storefront.

<figure><img src="/files/Qg9iBvQNHn32Pe9FXRrv" alt=""><figcaption></figcaption></figure>

Here, the developer can add in their own JavaScript code, taking advantage of Liquid for mixing in data from the current store, or from the current task's options.

{% hint style="info" %}
A task's JavaScript content only has access to to the `shop` and `options` Liquid variables. The rendering context is similar to that of [task subscriptions](/core/tasks/subscriptions#using-liquid); Liquid code here does not have access to any data related to events, and cannot dynamically respond to any information about the visitor's current request.
{% endhint %}

![](/files/V2OTgAXVAcjGUc75VK6N)


# Perform action runs in sequence

Mechanic's [run system](/core/runs) works asynchronously, performing as much work as possible, as quickly as possible. However, there are cases where it's important that actions run in a sequence – one after the other.

We support this with an advanced task setting called "Perform action runs in sequence", configured in two parts:

* **Perform action runs in sequence** – When enabled, Mechanic will only run one of the task's resulting actions at a time, performing them in the order in which they were generated.
* **Halt the sequence when one fails** – When this option is also enabled, Mechanic will only run the next action if the current action was performed successfully. If the action fails, all following actions will be marked as failed as well, with error messages explaining the situation.

{% hint style="warning" %}
Action run sequences are enforced within each task run. This means that a task could see more than one of its actions performed at the same time, if the task itself were to run multiple times, simultaneously.

To explain by example: a task that responds to mechanic/scheduler/10min, generating a sequence of 5 actions that each take 1 minute to run, will never see those actions overlap. However, if the task generated 15 actions instead, the actions would begin to overlap, as the task generates 15-minute action sequences every 10 minutes.
{% endhint %}

![](/files/xfz6Jr4YR7vnmrwyr7Yf)


# Import and export

Share and back up Mechanic tasks as JSON — import individually or in bulk, and contribute to the task library.

Mechanic tasks may be imported and exported as JSON, using the "Import" or "Export" button below the task editor. The JSON schema used for representing tasks is identical to that used by the **task library**, making it suitable for [contributing to the task library](/resources/task-library/contributing).

For local editing, Git history, previews, diffs, and safer publishing back to Mechanic, use [local task development with the Mechanic CLI](/resources/mechanic-cli). The CLI uses the same task JSON shape, with helper folders for editing Liquid and docs in separate files.

## Importing

Mechanic has the ability to import tasks from JSON individually and in bulk, from the "Import tasks" screen.

Each task loaded via this route may be saved as a new task, or – if the task name exactly matches the name of a task already in the Mechanic account – it may be saved *over* the existing task. This latter path provides a way for batches of updated tasks to be loaded into a Mechanic account all together, preserving the version history for each task.

To import one or more tasks from a JSON export, use the "Import tasks" button on the Mechanic home screen.

![](/files/KwWtDa3ns013fNOKwtcQ)

On the next screen, follow the prompts to load your JSON task exports into Mechanic.

### Importing in the task editor

When working in the task editor for a specific task, use the "Import" button to load in task JSON and have it applied to the current task.

![](/files/ObjxM5amDjFvfpVyLrMI)

## Exporting

When viewing the task list on the Mechanic home screen, use the "Export" button after selecting one or more tasks to copy a JSON export of all tasks to the clipboard. This export can be used with Mechanic's task import area, described above.

![](/files/1VygBP5u0lNIfFlUvQD2)

### Exporting from the task editor

When working in the task editor for a specific task, use the "Export" button to copy a JSON representation of the current task to the clipboard.

![](/files/iKAJMm223UFPxdmOKrWZ)


# User Form

Create custom input forms for manually-triggered Mechanic tasks — collect user input and pass it to your task code.

When a task subscribes to the **mechanic/user/form** event topic a "Run task" button is added to the task.

When the Run Task button is clicked the user is presented with a form that contains any [task options](/core/tasks/options) that have the `_userform` flag.

When submitted, an event is generated, to which only this task will respond. The user's input is available in Liquid as `input.<name>`.

<figure><img src="/files/ioHP4ZLAzTOUafOb93pA" alt=""><figcaption><p>User form on Run Task page</p></figcaption></figure>

{% hint style="info" %}
Click the link button beside the title of the form to copy a link to the form that you can share with your users
{% endhint %}

### Where User Form fields appear

User Form fields show up in:

* The task-level Run Task button for `mechanic/user/form`.
* The Run tasks page for Shopify admin action links (and equivalent run links) when exactly one task is selected.

See [Shopify admin action links](/core/shopify/admin-action-links) and [Run links](/platform/integrations/run-links) for details.

### Getting the user’s input in code

During a `mechanic/user/form` event, ad-hoc values are available as `input.<name>`. `event.data` also contains these values for backward compatibility.

```liquid
{% # These will appear on the run task user form %}
{% assign big_event = options.the_big_event__date_userform %}
{% assign color_for_big_event = options.color_for_big_event__color_required_userform %}

{% # This will NOT appear on the run task user form %}
{% assign level = options.level__select_o1_low_o2_high %}

{% if event.topic == "mechanic/user/form" %}
  {% # we need to get the value from user input, we don't want the value from the task option %}
  {% assign big_event = input.the_big_event %}
  {% assign color_for_big_event = input.color_for_big_event %}

{% endif %}

{% action "echo" big_event, level, color_for_big_event %}
```

{% hint style="info" %}
Note: `event.data` contains submitted values for backward compatibility. Prefer `input.<name>` going forward, and use `options.*` for stored task options.
{% endhint %}

User form options also appear on the Run tasks page for Shopify admin action links (mechanic/user/{resource} topics). In that context, `input.<name>` is still the recommended way to access user input.


# Actions

Actions are how Mechanic tasks make changes — Shopify mutations, emails, HTTP requests, file generation, and more.

An **action** is an operation with an effect — tagging an order, sending an email, uploading a file to FTP, or calling an external API. Tasks produce actions by rendering action definitions in Liquid, which means a single task can dynamically generate any number of actions based on the data it processes.

Actions run after the task code completes. Each action has a **type** (specifying the operation) and **options** (providing specifics). Action definitions can be constructed using the [**action tag**](/platform/liquid/tags/action) or as raw [**action objects**](/core/tasks/code/action-objects) in JSON.

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

If you don't need the follow-up event for a particular action, add `__perform_event: false` to that action to skip emitting mechanic/actions/perform while still performing the action.

To attach meta while using any of the action tag syntaxes, add `__meta` and Mechanic will move that value into the action's meta field.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

## Action types

An **action type** determines the class of operation to be performed. While actions may vary greatly, there are only a few action types. Shopify is treated as a first-class action because it's the primary API Mechanic automates against.

| Action                           | Purpose                                                                              |
| -------------------------------- | ------------------------------------------------------------------------------------ |
| [Cache](/core/actions/cache)     | Performing operations on the store's Mechanic cache                                  |
| [Echo](/core/actions/echo)       | Debugging; displays the options that it is provided, with no side-effects            |
| [Email](/core/actions/email)     | Sending transactional email                                                          |
| [Event](/core/actions/event)     | Generating custom events in the [User event domain](/platform/events/topics#user)    |
| [Files](/core/actions/files)     | Generating files of various types, storing them at a temporary Mechanic-provided URL |
| [FTP](/core/actions/ftp)         | Performing FTP file uploads and downloads                                            |
| [HTTP](/core/actions/http)       | Performing HTTP requests                                                             |
| [Shopify](/core/actions/shopify) | Sending requests to the Shopify Admin API                                            |

### Integrations

Mechanic also maintains a set of integration actions for third-party services beyond Shopify.

| Action                                                      | Integrated service               | Purpose                                                                        |
| ----------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------ |
| [Airtable](/core/actions/integrations/airtable)             | Airtable                         | Create and edit tables and records in Airtable                                 |
| [Flow](/core/actions/integrations/flow)                     | Shopify Flow                     | Sending customer, order, product, and general triggers to Shopify Flow         |
| [Google](/core/actions/integrations/google)                 | Google (Drive and Google Sheets) | Advanced integration with the Google Drive and Sheets APIs                     |
| [Google Drive](/core/actions/integrations/google-drive)     | Google Drive                     | Upload files to Google Drive                                                   |
| [Google Sheets](/core/actions/integrations/google-sheets)   | Google Sheets                    | Create, Update, Export Google Sheets                                           |
| [Report Toaster](/core/actions/integrations/report-toaster) | Report Toaster                   | Requesting reports from Report Toaster, or updating data within Report Toaster |
| [Slack](/core/actions/integrations/slack)                   | Slack                            | Posting messages to Slack                                                      |


# Cache

The **Cache** action allows developers to interact with the store's Mechanic [**cache**](/platform/cache), using commands inspired by Redis. Cache entries have a **key**, a **value** containing up to 256 kilobytes, and a **ttl** value ("Time To Live") in seconds, defaulting to the maximum of 60 days (i.e. 5184000 seconds).

{% hint style="info" %}
Cache actions (like all actions) are performed after their task run is completed. The results of Cache actions therefore aren't reflected during the task run that generates them.
{% endhint %}

## Options

This action supports two styles of options: a more verbose nested structure, and a simpler set of positional arguments.

All commands must define a cache key, matching the regular expression `/^[a-z0-9_:\-\.\/]+$/i`.

### Verbose options

In this option style, the cache command is given as the root key of the options object. The root value is itself an option, containing the arguments needed for the selected cache command.

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "cache" %}
  {
    "incr": {
      "key": "foo",
      "ttl": 600
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

### Positional options

In this option style, the cache command and its arguments are given in a list. Use the cache command reference below to find the argument order required for each command.

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "cache", "incr", "foo" %}
```

{% endtab %}
{% endtabs %}

## Expiration

Each cache entry is given a default TTL value of 60 days, or 5184000 seconds. (A cache entry's TTL may not exceed 60 days.)

A cache command will always reset the entry's TTL value upon execution, regardless of the TTL's original value.

## Commands

The required arguments for each command are given below, in the order in which they are supported for [positional options](#positional-options).

When a command is given using [verbose options](#verbose-options), the `ttl` value (in seconds) is always supported.

### set

Stores a value. Requires `key` and `value`. The stored value may be any JSON object.

### setex

Using a defined TTL (an expiration interval) given in seconds, stores a value. Requires `key`, `ttl`, and `value`. The stored value may be any JSON object.

{% hint style="info" %}
The "setex" command has the same net functionality as "set", but it does have one difference: because "setex" requires an explicit `ttl` value, it's possible to use "setex" to express an expiring value using a single line of Liquid. The same result could be achieved with "set", but it would require using verbose options.

```liquid
{% action "cache", "setex", "foo", 5, "bar" %}
```

{% endhint %}

### del

Deletes a stored key. Requires `key`.

### incr

Increments a numeric key by 1. Requires `key`. If the key is not already set, the value before incrementing will be assumed to be 0.

### incrby

Increments a numeric key by the value of your choice. Requires `key`, and an integer `increment`. If the key is not already set, the value before incrementing will be assumed to be 0.

### decr

Decrements a numeric key by 1. Requires `key`. If the key is not already set, the value before incrementing will be assumed to be 0.

### decrby

Decrements a numeric key by the value of your choice. Requires `key`, and an integer `decrement`. If the key is not already set, the value before incrementing will be assumed to be 0.

## Examples

### Set a value, auto-expiring in 60 days

{% tabs %}
{% tab title="Verbose options" %}

```liquid
{% action "cache" %}
  {
    "set": {
      "key": "foo",
      "value": 5
    }
  }
{% endaction %}
```

{% endtab %}

{% tab title="Positional options" %}

```liquid
{% action "cache", "set", "foo", 5 %}
```

{% endtab %}
{% endtabs %}

### Set a value, explicitly expiring in 1 minute

{% tabs %}
{% tab title="Verbose options" %}

```liquid
{% action "cache" %}
  {
    "setex": {
      "key": "foo",
      "ttl": 60,
      "value": 5
    }
  }
{% endaction %}
```

{% endtab %}

{% tab title="Positional options" %}

```liquid
{% action "cache", "setex", "foo", 60, 5 %}
```

{% endtab %}
{% endtabs %}

### Clear a value

{% tabs %}
{% tab title="Verbose options" %}

```liquid
{% action "cache" %}
  {
    "del": {
      "key": "foo"
    }
  }
{% endaction %}
```

{% endtab %}

{% tab title="Positional options" %}

```liquid
{% action "cache", "del", "foo" %}
```

{% endtab %}
{% endtabs %}


# Echo

The **Echo** action has no effects: it returns the options that are given. This action can be useful for testing or debugging, by temporarily replacing some other action with an Echo action having the same options. In this way, a developer can safely get feedback on what data is in play, without side effects.

## Options

This action accepts any and all options, restricted only in that they must be valid JSON values (as with all results of [task code](/core/tasks/code)).

### Forcing an error

If the Echo action is given a `"__error"` option, it will raise that error when the action run is performed. Use this feature when it's useful to indicate an issue with a task run, without marking the entire task run as a failure (as would be the case when using an [error object](/core/tasks/code/error-objects)).

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "echo", __error: "Forcing an error!" %}
```

{% endtab %}
{% endtabs %}

![](/files/F3H68N5ERe1I18MIPxId)

## Examples

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "echo", foo: "bar", baz: "qux" %}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "echo", "foo", "bar", "baz" %}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "echo", "foo" %}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "echo" %}
  {
    "foo": "bar",
    "baz": "qux"
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# Email

Send automated transactional emails from Shopify using Mechanic — HTML templates, attachments, and custom sender domains.

The **Email** action is for sending email. ✅ It supports the store's [**email templates**](/platform/email/templates), and supports attachments constructed by [**file generators**](/core/actions/file-generators).

## Restrictions

Mechanic sends email via [Postmark](https://postmarkapp.com/), our email provider. Currently, Mechanic only supports Postmark's transactional message stream, which means that marketing and other bulk mail may not be sent. To learn more about what is and isn't a transactional message, see Postmark's article: ["What are Transactional emails?"](https://postmarkapp.com/support/article/804-what-are-transactional-emails).

## Options

| Option              | Description                                                                                                             |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `to`                | Required; an array or comma-delimited string of recipient addresses                                                     |
| `subject`           | Required; a string specifying the message subject                                                                       |
| `body`              | Required; an HTML string of body content; supports HTML and CSS                                                         |
| `cc`                | Optional; an array or comma-delimited string of cc addresses                                                            |
| `bcc`               | Optional; an array or comma-delimited string of bcc addresses                                                           |
| `reply_to`          | Optional; a single reply-to address                                                                                     |
| `from_display_name` | Optional; a string controlling the name (but not the address) of the sender                                             |
| `headers`           | Optional; a hash of email header strings and value strings                                                              |
| `template`          | Optional; a string naming an email template from the current Mechanic account                                           |
| `attachments`       | Optional; an object specifying files to attach, using [file generators](/core/actions/file-generators)                  |
| `...`               | Additional options may be provided, and will be made available to email templates as variables, named after each option |

## Message formatting

### HTML and CSS

Mechanic parses each email body for HTML and CSS, allowing authors to use `<style>` tags without having to think about email client compatibility.

{% hint style="info" %}
If you're simply trying to add formatted text and aren't ready to dig into the code yourself, try using a tool like [wordtohtml.net](https://wordtohtml.net/) to quickly generate usable HTML.
{% endhint %}

### Embedded images

Images may be embedded using the `<img>` tag, but must be hosted independently. Shopify provides basic file hosting, appropriate for uploading images for use with Mechanic emails. To learn more, see [Uploading files to your website](https://help.shopify.com/en/manual/shopify-admin/productivity-tools/file-uploads).

## Changing the sender address

This action only supports sending from a single address (regardless of the sender name, as controlled by the `from_display_name` option).

By default, the sender address is a Mechanic address based on the store's myshopify.com subdomain. For example, the store example.myshopify.com will default to having its mail sent from <example@mail.usemechanic.com>.

Changing the sender address involves adding it to the store's Mechanic account, and then configuring the email domain name with some DNS records for verification.

For more on this, see [Custom email domain](/platform/email/custom-email-domain).

## Template

To achieve easily reusable headers and footers, Mechanic can be configured with one or more email templates, available in the Mechanic account settings. To learn more about configuring email templates, see [Email templates](/platform/email/templates).

To use a specific email template with the Email action, use the `template` option to specify the name of the desired email template.

### Creating email template variables

All options used with the Email action will be made available as Liquid variables for the email template. This means that standard options may be used, like `{{ subject }}` and `{{ body }}`, and also custom options: passing in an `"order_data"` option, containing order data, may allow the email template to show the order name via `{{ order_data.name }}`.

{% hint style="warning" %}
Note that custom options, like all task options, must be provided using standard JSON. This means that the data made available to email templates will be derived from plain JSON values.

For example, consider this action:

```liquid
{% action "email" %}
  {
    "to": "customer@example.com",
    "subject": "Thanks for your order!",
    "template": "order_acknowledgement",
    "order_data": {{ order | json }}
  }
{% endaction %}
```

The template named "order\_acknowledgement" could include the following Liquid, and get the expected results:

```liquid
This is the first item: {{ order_data.line_items.first.title }}
```

But, because `order_data` is a plain [hash](/platform/liquid/keyword-literals/hash) based entirely on JSON data, instead of being an enhanced order object (see [Environment variables](/core/tasks/code/environment-variables)), the following Liquid usage would fail:

```liquid
Remember order {{ order_data.customer.orders.any.first.number }}, your first ever?
```

{% endhint %}

## Attachments

This action supports attachments given in Mechanic's file generator format. This structure allows the sender to construct a variety of files, including ad-hoc text-based files, PDFs rendered from HTML, files dynamically downloaded from external locations, and ZIP files containing any other files.

For more on this, see [File generators](/core/actions/file-generators).

## Examples

### Simple

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "email" %}
  {
    "to": "hello@example.com",
    "subject": "Hello world",
    "body": "It's a mighty fine day!",
    "reply_to": {{ shop.customer_email | json }},
    "from_display_name": {{ shop.name | json }}
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

### Using HTML

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% capture email_body %}
  <b>Hello!</b>

  It's fantastic to see you!
{% endcapture %}

{% action "email" %}
  {
    "to": "hello@example.com",
    "subject": "Hello world",
    "body": {{ email_body | unindent | strip | newline_to_br | json }}
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

### Using attachments

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "email" %}
  {
    "to": "test@example.com",
    "subject": "An image test",
    "body": "Please see attached. :)",
    "attachments": {
      "a_configured_image_from_the_web.png": {
        "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
      }
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

## Related

* [Email](/platform/email) — email platform features, including custom domains, DMARC, and receiving inbound email
* [Email templates](/platform/email/templates) — reusable email layouts and styling
* [File generators](/core/actions/file-generators) — generate PDFs, CSVs, and other attachments for your emails


# Event

The **Event** action is for generating custom events in the [**User event domain**](/platform/events/topics#user-1). It's used to queue up follow-up work, either immediately or in the future, and can be useful when designing complex workloads, separating work between tasks.

Events generated by this action may be responded to by other tasks, or by the task that generated this action.

Events generated by this action are [**child events**](/core/events/parent-and-child-events) of the event responsible for the current action.

## Options

| Option     | Description                                                                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `topic`    | Required; a string specifying an [event topic](/core/events/topics) of the form "user/\*/\*"                                                |
| `data`     | Required; any JSON value (including `null`), to be used as the event data                                                                   |
| `run_at`   | Optional; a Unix timestamp integer, or any string that can be parsed as a time                                                              |
| `task_ids` | Optional, cannot be used with `task_id`; an array of task UUID strings, specifying which tasks are allowed to respond to this event         |
| `task_id`  | Optional, cannot be used with `task_ids`; a string containing a single task UUID, specifying which task is allowed to respond to this event |

### Notes

If a `run_at` value specifies a time in the past, the new event will be run immediately.

Tasks specified by `task_ids` or `task_id` must subscribe to the event topic being used. As with all subscriptions, [offsets](/core/tasks/subscriptions#offsets) may be used, and will be respected.

## Examples

### Using the Event tag

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% assign data = hash %}
{% assign data["foo"] = "bar" %}

{% action "event", topic: "user/foo/bar", data: data %}
```

{% endtab %}
{% endtabs %}

### Using specific tasks

#### Using task\_id

Uses the optional `task_id` parameter to control which **singular** task is allowed to respond to this event.

That task must be subscribed to the event topic being used.

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% assign data = hash %}
{% assign data["foo"] = "bar" %}
{% assign task_id = "293b7040-6689-4eb1-8b5d-64f4d33eb2ae" %}
{% comment %} For multiple tasks use `task_ids` {% endcomment %}

{% action "event", topic: "user/foo/bar", data: data, task_id: task_id %}
```

{% endtab %}
{% endtabs %}

#### Notes

You can limit a task to itself by referencing it's own task.id

See [**options**](/core/tasks/options) to have a user configurable input instead of hardcoding the task id(s).

### Scheduling future events

#### Using run\_at

This example uses the `run_at` option to run the task at a later scheduled time.

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% assign one_day_in_seconds = 60 | times: 60 | times: 24 %}

{% action "event" %}
  {
    "topic": "user/foo/bar",
    "task_id": {{ task.id | json }},
    "run_at": {{ "now" | date: "%s" | plus: one_day_in_seconds | json }},
    "data": {
      "foo": "bar"
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

This task emails a customer daily until their order is paid. It works by scheduling a follow-up run of the same task, one day in the future, using the `run_at` option.

**Subscriptions**

```liquid
shopify/orders/create
user/orders/unpaid_reminder
```

**Code**

```liquid
{% if event.preview %}
  {% assign order = hash %}
  {% assign order["id"] = 1234568790 %}
  {% assign order["name"] = "#1234" %}
{% elsif event.topic == "user/orders/unpaid_reminder" %}
  {% assign order = shop.orders[event.data.order_id] %}
{% endif %}

{% unless order.financial_status == "paid" %}
  {% action "email" %}
    {
      "to": {{ order.email | json }},
      "reply_to": {{ shop.customer_email | json }},
      "subject": "Order {{ order.name }} still needs to be paid",
      "body": "Please get in touch, stat!",
      "from_display_name": {{ shop.name | json }}
    }
  {% endaction %}

  {% assign one_day_in_seconds = 60 | times: 60 | times: 24 %}

  {% action "event" %}
    {
      "topic": "user/orders/unpaid_reminder",
      "task_id": {{ task.id | json }},
      "run_at": {{ "now" | date: "%s" | plus: one_day_in_seconds | json }},
      "data": {
        "order_id": {{ order.id | json }}
      }
    }
  {% endaction %}
{% endunless %}
```

#### Using subscription offsets

This task emails a customer daily until their order is paid. It works by firing the follow-up event immediately, using a subscription offset to respond to it a day later.

**Subscriptions**

```liquid
shopify/orders/create
user/orders/unpaid_reminder+1.day
```

**Code**

```liquid
{% if event.preview %}
  {% assign order = hash %}
  {% assign order["id"] = 1234568790 %}
  {% assign order["name"] = "#1234" %}
{% elsif event.topic == "user/orders/unpaid_reminder" %}
  {% assign order = shop.orders[event.data.order_id] %}
{% endif %}

{% unless order.financial_status == "paid" %}
  {% action "email" %}
    {
      "to": {{ order.email | json }},
      "reply_to": {{ shop.customer_email | json }},
      "subject": "Order {{ order.name }} still needs to be paid",
      "body": "Please get in touch, stat!",
      "from_display_name": {{ shop.name | json }}
    }
  {% endaction %}

  {% assign one_day_in_seconds = 60 | times: 60 | times: 24 %}

  {% action "event" %}
    {
      "topic": "user/orders/unpaid_reminder",
      "task_id": {{ task.id | json }},
      "data": {
        "order_id": {{ order.id | json }}
      }
    }
  {% endaction %}
{% endunless %}
```


# Files

The **Files** action evaluates its options using [**file generators**](/core/actions/file-generators), temporarily storing the resulting files and making them available via a randomized Mechanic URL.

This action is most useful in concert with [mechanic/actions/perform](/techniques/responding-to-action-results), by which a task may take the resulting file URLs and pass them on to another service. Used by itself, this action can also be useful for quickly testing file generators.

## Options

This action accepts a JSON object, whose keys are filenames and whose values are [file generators](/core/actions/file-generators). In this way, many files may be defined and generated by a single Files action.

## Result

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

A Files action returns an object having the same keys (i.e. filenames) as its input. Each value is an object, having the following properties:

| File property | Description                                                                                           |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| `expires_at`  | An ISO8601 timestamp, specifying when the file will expire                                            |
| `mime_type`   | The [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) of the generated file |
| `name`        | The filename, as given in the original action options                                                 |
| `size`        | The size of the generated file, in bytes                                                              |
| `url`         | The URL at which this file will be available, until it expires                                        |

## Example

This task generates a variety of files. It then re-invokes itself (via mechanic/actions/perform), sending an email containing links to each of the generated files.

**Subscriptions**

```liquid
mechanic/user/trigger
mechanic/actions/perform
```

**Code**

```liquid
{% if event.topic == "mechanic/user/trigger" %}
  {% action "files" %}
    {
      "journal.txt": "hello world!",
      "table.csv": "Title,SKU\nRed T-Shirt,TEE-R",
      "invoice.pdf": {
        "pdf": {
          "html": "<h1>Order #12345</h1>\n<p>It's due!</p>"
        }
      },
      "secure.zip": {
        "zip": {
          "password": "opensesame",
          "files": {
            "confirmations.txt": "this data is protected with zipcrypto encryption"
          }
        }
      },
      "external.jpg": {
        "url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"
      }
    }
  {% endaction %}
{% elsif event.topic == "mechanic/actions/perform" and action.type == "files" %}
  {% capture email_body %}
    <p>The following file(s) have been generated:</p>
    <ul>
      {% for keyval in action.run.result %}
        {% assign filename = keyval[0] %}
        {% assign file = keyval[1] %}
        <li><a href="{{ file.url }}">{{ filename }}</a> ({{ file.size }} bytes)</li>
      {% endfor %}
    </ul>
    <p>-Mechanic</p>
  {% endcapture %}

  {% action "email" %}
    {
      "to": "isaac@lightward.com",
      "subject": {{ action.run.result | size | append: " file(s) generated" | json }},
      "body": {{ email_body | json }}
    }
  {% endaction %}
{% endif %}
```


# FTP

Upload and download files via FTP, FTPS, or SFTP.

The **FTP** action can upload and download files via [FTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol), [FTPS](https://en.wikipedia.org/wiki/FTPS), or [SFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol). The files to be uploaded are evaluated using [**file generators**](/core/actions/file-generators). Downloaded file data is available either as an UTF-8 string, or as a base64-encoded string, and can be used in followup task runs via [mechanic/actions/perform](/techniques/responding-to-action-results).

A connecting service like [Couchdrop](https://couchdrop.io/) can be used to relay these uploads on to other cloud locations, like Dropbox, Google Drive, and Amazon S3.

A single FTP action may download a maximum of 20MB of data, across all downloaded files.

## Options

### General options

<table><thead><tr><th width="204.33333333333331">Option</th><th width="186">Type</th><th>Notes</th></tr></thead><tbody><tr><td><code>protocol</code></td><td><code>"ftp"</code>, <code>"ftps"</code>, or <code>"sftp"</code></td><td>The protocol to use for connection; inferred if omitted</td></tr><tr><td><code>host</code></td><td>String, required</td><td>The hostname or IP address of the destination server</td></tr><tr><td><code>port</code></td><td>Number, optional</td><td>The server port to connect to</td></tr><tr><td><code>user</code></td><td>String, required</td><td>The username for authentication</td></tr><tr><td><code>password</code></td><td>String, optional</td><td>The password for authentication</td></tr><tr><td><code>uploads</code></td><td>Hash, optional</td><td>An object whose keys are file paths (relative or absolute), and whose values are <a href="/pages/-MXYcOuvq9zSw-QKgyRF">file generators</a></td></tr><tr><td><code>downloads</code></td><td>Array, optional</td><td>File path strings (relative or absolute) to download</td></tr></tbody></table>

### FTP options

| Option | Type             | Notes                                           |
| ------ | ---------------- | ----------------------------------------------- |
| `mode` | String, optional | May be set to `"ascii"`; defaults to `"binary"` |

### FTPS options

| Option   | Type              | Notes                                                  |
| -------- | ----------------- | ------------------------------------------------------ |
| `verify` | Boolean, optional | May be set to `false` to ignore SSL certificate errors |

### SFTP options

| Option            | Type              | Notes                                                                                                                                          |
| ----------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `private_key_pem` | String, optional  | A complete PEM-formatted private key for authentication                                                                                        |
| `verify`          | Boolean, optional | May be set to `true` in combination with `"known_hosts"` to validate the host                                                                  |
| `known_hosts`     | String, optional  | An sshd-compatible known\_hosts file ([docs](https://linux.die.net/man/8/sshd), [helpful article](https://linuxhandbook.com/known-hosts-file)) |

### User authentication

The `user` option is always required.

When connecting to an FTP or FTPS server, authenticate with the `password` option.

When connecting to an SFTP server, authenticate using either `password` or `private_key_pem`, or both. PEM certificates may be given directly in the task code:

```liquid
{% capture private_key_pem %}
-----BEGIN OPENSSH PRIVATE KEY-----
l0UGrDQWWbOpUsLENHwD5ya478pmRXarmDj5Wh31B54nmuq7be4ZKD5eh9nEV42JCl4mX6
...
pZ/WFoT82brhooSfJDue14C0Y=
-----END OPENSSH PRIVATE KEY-----
{% endcapture %}

{% action "ftp" %}
  {
    "host": "example.com",
    "port": 22,
    "user": "sftp_user",
    "private_key_pem": {{ private_key_pem | json }}
    "uploads": {
      "success.txt": "hooray!"
    }
  }
{% endaction %}
```

The FTP action supports [shop secrets](/platform/globals-and-secrets) for connection settings. Prefer storing passwords and private keys as shop secrets, then referencing them from the action:

```liquid
{% action "ftp" %}
  {
    "protocol": "sftp",
    "host": "example.com",
    "port": 22,
    "user": {{ globals.sftp_username | json }},
    "password": {{ secrets.sftp_password | json }},
    "uploads": {
      "success.txt": "hooray!"
    }
  }
{% endaction %}
```

Secret references may come directly from `secrets.some_key` or from a task option such as `options.sftp_password__secret_required`.

The FTP action resolves secrets only for these connection fields: `host`, `user`, `password`, `private_key`, `private_key_pem`, and `known_hosts`. Upload paths, download paths, filenames, generated file contents, and other FTP options keep secret references as placeholders.

Stored action data, errors, previews, and logs keep placeholders or redacted values. If an FTP action resolves any secret, base64 diagnostic fields such as `data_base64` may be replaced with `__mechanic_secret_value_redacted__`.

### Uploads and downloads

Both `uploads` and `downloads` allow the task author to define file paths. If only the filename is given (e.g. `"sample.pdf"`), the file will be resolved in the home directory of the user. If a relative path (e.g. `"subdirectory/sample.pdf"`) or absolute path (e.g. `"/tmp/sample.pdf"`) is given, it will be respected accordingly.

Each individual file operation (i.e. each upload or download) will be attempted a maximum of 3 times within the FTP/FTPS/SFTP session, retrying if an error occurs during upload or download.

#### Example

This example action results in (a) an upload to an absolute path, starting from the server root, (b) an upload to a nested directory within the user's home folder, and (c) an upload to a nested directory in another user's home folder (which may fail, depending on filesystem permissions).

```liquid
{% action "ftp" %}
  {
    ...
    "uploads": {
      "/absolute/path/to/success.txt": "hooray!",
      "relative/path/to/success.txt": "hooray!",
      "../another/relative/path/to/success.txt": "hooray!"
    }
  }
{% endaction %}
```

## Result

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

An FTP action returns the following data structure, most useful in combination with mechanic/actions/perform (see [Responding to action results](/techniques/responding-to-action-results)):

```json
{
  "log": "connect: ftp.couchdrop.io, 21\n< 220 Couchdrop FTPD\n> USER ********\n< 331 Username ok, send password.\n> PASS ********\n< 230 Welcome ********\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,153).\n> STOR journal.txt\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,98).\n> STOR table.csv\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,135).\n> STOR invoice.pdf\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,101).\n> STOR secure.zip\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,100).\n> STOR external.jpg\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,183).\n> RETR journal.txt\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n> TYPE I\n< 200 Type set to: Binary.\n> PASV\n< 227 Entering passive mode (178,128,9,71,234,149).\n> RETR table.csv\n< 125 Data connection already open. Transfer starting.\n< 226 Transfer complete.\n> TYPE I\n< 200 Type set to: Binary.\n",
  "uploads": {
    "invoice.pdf": {
      "size": 7232
    },
    "secure.zip": {
      "size": 205
    },
    "external.jpg": {
      "size": 27661
    }
  },
  "downloads": {
    "journal.txt": {
      "size": 12,
      "data": "hello world!",
      "data_base64": "aGVsbG8gd29ybGQh"
    },
    "table.csv": {
      "size": 27,
      "data": "Title,SKU\nRed T-Shirt,TEE-R",
      "data_base64": "VGl0bGUsU0tVClJlZCBULVNoaXJ0LFRFRS1S"
    }
  }
}
```

Note that each uploaded and downloaded file is keyed by the path provided for that file in the action's options. Downloaded file data is available as a UTF-8 string; for binary data that cannot be represented in UTF-8, use the base64-encoded version, possibly in concert with the [decode\_base64](/platform/liquid/filters#base-64-decode_base64) filter.

## Testing

If a server is unavailable for testing, consider using [Couchdrop](https://couchdrop.io/), with [their hosted storage service](https://couchdrop.io/features/hosted-storage). This is a (nearly) configuration-free avenue for testing, using my.couchdrop.io for FTP, FTPS, or SFTP.

Alternatively, [ngrok](https://ngrok.com/) can be used to create a public tunnel to a local FTP or SSH server. By running `ngrok tcp 22` (adjusting for the appropriate local port), ngrok will generate a temporary public host and port that's appropriate for use while testing.

Uploads are processed before downloads; it can be useful to test by uploading a file, and then immediately downloading it again:

```liquid
{% action "ftp" %}
  {
    "host": "ftp.couchdrop.io",
    ...
    "uploads": {
      "hello-world.txt": "hello world!"
    },
    "downloads": [
      "hello-world.txt"
    ]
  }
{% endaction %}
```

## Example

This task compiles all SKUs with their titles and prices, and uploads it as a CSV every night or on demand.

**Subscriptions**

```
mechanic/scheduler/daily
mechanic/user/trigger
```

**Code**

```liquid
{% assign csv_rows = array %}

{% assign header = "SKU,Title,Price" | split: "," %}
{% assign csv_rows[0] = header %}

{% for product in shop.products %}
  {% for variant in product.variants %}
    {% assign title = variant.title %}
    {% if title == "Default Title" %}
      {% assign title = product.title %}
    {% endif %}

    {% assign row = array %}
    {% assign row[row.size] = variant.sku %}
    {% assign row[row.size] = title %}
    {% assign row[row.size] = variant.price %}

    {% assign csv_rows[csv_rows.size] = row %}
  {% endfor %}
{% endfor %}

{% capture filename %}product-export-{{ "now" | date: "%Y-%m-%d" }}.csv{% endcapture %}

{% action "ftp" %}
  {
    "host": "example.com",
    "port": 21,
    "user": "anonymous",
    "password": null,
    "uploads": {
      {{ filename | json }}: {{ csv_rows | csv | json }}
    }
  }
{% endaction %}
```


# HTTP

Make HTTP requests from Mechanic tasks to call external APIs, send webhooks, and integrate Shopify with third-party services.

The **HTTP** action performs HTTP requests. It is commonly used to invoke third-party APIs.

To use the response from an HTTP action, add a task subscription to [mechanic/actions/perform](/techniques/responding-to-action-results).

{% hint style="info" %}
When developing task code, verify your HTTP action's behavior with [webhook.site](https://webhook.site/) (making sure not to share sensitive information with this service).
{% endhint %}

## Options

<table><thead><tr><th width="211.33333333333331">Option</th><th width="196">Type</th><th>Notes</th></tr></thead><tbody><tr><td><code>method</code></td><td>String, required</td><td>Must be one of <code>"options"</code>, <code>"head"</code>, <code>"get"</code>, <code>"post"</code>, <code>"put"</code>, <code>"patch"</code>, or <code>"delete"</code></td></tr><tr><td><code>url</code></td><td>String, required</td><td>Must start with <code>https://</code> or <code>http://</code></td></tr><tr><td><code>body</code></td><td>String, required for non-GET requests</td><td>Format varies, see below</td></tr><tr><td><code>files</code></td><td>Hash, optional</td><td>May be set to a JSON object, mapping filenames to <a href="/pages/-MXYcOuvq9zSw-QKgyRF">file generators</a></td></tr><tr><td><code>headers</code></td><td>Hash, optional</td><td>May be set to a JSON object, mapping header names to header values</td></tr><tr><td><code>follow_redirects</code></td><td>Boolean, optional</td><td>Defaults to <code>true</code>, may be set to <code>false</code>; controls whether or not 3xx responses with <code>Location</code> headers are automatically followed to their destination</td></tr><tr><td><code>proxy</code></td><td>String, optional</td><td>May be a proxy URI string beginning with <code>https://</code>, <code>http://</code>, or <code>socks5://</code>; see "Using a proxy" below</td></tr><tr><td><code>verify</code></td><td>Boolean, optional</td><td>May be set to <code>false</code> to disable SSL certificate verification</td></tr><tr><td><code>error_on_5xx</code></td><td>Boolean, optional</td><td>May be set to <code>true</code> to have 5xx HTTP response codes be considered action errors</td></tr></tbody></table>

### Request format

The HTTP action has intelligently varying behavior, based on the presence and value of the Content-Type header, and the data type of the `body` option.

#### JSON

If the Content-Type header is unspecified or set to `application/json`, and if the `body` option is set to a JSON object or array, the request body will be automatically serialized to a JSON string, and the request will contain a Content-Type header set to `application/json`.

#### Form-encoded data

If the `files` option is given, its contents will be evaluated for [file generators](/core/actions/file-generators), and the results will be used to construct a `multipart/form-data` upload request, combining generated files with any key-value pairs found in the `body` option.

This is also the pattern to use when Shopify gives you a staged upload URL, such as when preparing JSONL variables for a [bulk operation mutation](/resources/tutorials/bulk-operation-mutations). Do not manually set the multipart `Content-Type` header; Mechanic sets the multipart boundary automatically. Generated files have a [20MB per-file limit](/core/actions/file-generators#maximum-filesize).

```liquid
{% action "http" %}
  {
    "method": "post",
    "url": "https://postman-echo.com/post",
    "body": {
      "hello": "world"
    },
    "files": {
      "robots.txt": {
        "url": "https://www.shopify.com/robots.txt"
      }
    }
  }
{% endaction %}
```

If the `files` option is not given, and if the Content-Type header is set to `application/x-www-form-urlencoded`, and if the `body` option is set to a JSON object or array, the request body will be serialized to a form-encoded string.

### Basic authentication

To authenticate a request using [the Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) and the "Basic" authentication type, use something like this:

```liquid
{% assign username = "guest" %}
{% assign password = "guest" %}
{% assign authorization_header = username | append: ":" | append: password | base64 | prepend: "Basic " %}

{% action "http" %}
  {
    "method": "get",
    "url": "https://jigsaw.w3.org/HTTP/Basic/",
    "headers": {
      "Authorization": {{ authorization_header | json }}
    }
  }
{% endaction %}
```

### Using secrets

The HTTP action supports [shop secrets](/platform/globals-and-secrets). It turns secret references in action options into raw values immediately before sending the request, while stored action data, previews, errors, and logs keep placeholders or redacted values.

```liquid
{% action "http" %}
  {
    "method": "post",
    "url": "https://api.example.com/orders",
    "headers": {
      "Authorization": {{ "Bearer " | append: secrets.api_token | json }}
    },
    "body": {
      "username": {{ globals.account_username | json }},
      "password": {{ secrets.account_password | json }}
    }
  }
{% endaction %}
```

Secret references may come directly from `secrets.some_key` or from a task option such as `options.api_token__secret_required`. This applies to HTTP request options such as `url`, `headers`, `body`, and `proxy`. HTTP validation runs after supported secret references are resolved, so invalid resolved header values or URLs still fail validation.

If an HTTP action resolves any secret, base64 diagnostic fields such as `body_base64` may be replaced with `__mechanic_secret_value_redacted__` to avoid exposing encoded secret values.

### Using a proxy

The HTTP action supports HTTPS, HTTP, and SOCKS5 proxy connections via the `"proxy"` option, set to a URI string beginning with `https://`, `http://`, or `socks5://`. When configured, Mechanic will open a connection to your proxy server, and pass your request through that connection.

{% code title="Example HTTP action using a proxy" %}

```liquid
{% action "http" %}
  {
    "method": "get",
    "url": "https://api.ipify.org?format=json",
    "proxy": "socks5://user:password@host.domain:port"
  }
{% endaction %}
```

{% endcode %}

{% hint style="warning" %}
We recommend using an HTTPS proxy server (rather than HTTP or SOCKS5) for a secure connection between Mechanic and your proxy. [QuotaGuard Shield](https://www.quotaguard.com/quotaguard-shield/) is a good option for this kind of service.
{% endhint %}

{% hint style="info" %}
Mechanic does not use static IP addresses for outbound requests. Using a connection proxy for your HTTP actions can allow you to control the client IP address of your API requests, for API vendors that require fixed IPs.
{% endhint %}

## Result

{% hint style="info" %}
In Mechanic, actions are performed after their originating task run concludes. Actions are not performed inline during the task's Liquid rendering.

To inspect and respond to the results of an HTTP action, add a task subscription to mechanic/actions/perform, allowing the action to re-invoke the task with the action result data.

Learn more: [Responding to action results](/techniques/responding-to-action-results)
{% endhint %}

An HTTP action returns an object containing the following keys:

| File property | Description                                                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `status`      | An integer, specifying the response code                                                                                     |
| `headers`     | An object containing response headers, where each key is a string and each value is an array of values found for that header |
| `body`        | The interpreted value of the response body; see below                                                                        |
| `body_base64` | The original response body, encoded using base64                                                                             |

### Response headers

Because HTTP allows for the same header name to be present multiple times, this action's result specifies an array for each response header – even if the header was only present once.

To retrieve a specific header in a task responding to [mechanic/actions/perform](/techniques/responding-to-action-results), use something like this:

```liquid
{% log response_type_header: action.run.result.headers['content-type'][0] %}
```

### Response body

If the response contained a Content-Type header set to `application/json`, the `body` result value will be the result of parsing the response body for JSON.

For all other cases, the `body` result value will be an UTF8 string, regardless of the response body's original encoding. To access the response body in its original encoding, use the `body_base64` result value, passing it through the [decode\_base64](/core/actions/http) Liquid filter if necessary.

### Handling errors

By default, this action will consider any valid HTTP response to be a success, regardless of its response code.

However, because 5xx responses should often be considered a retryable error, this action supports the `error_on_5xx` option. When set to `true`, this action will interpret any 5xx responses as an action error.

As with all runs, HTTP action errors are subject to [Mechanic's retry policy](/core/runs/retries).

## Example

This task prompts the user for text input, and submits it to a public API that returns everything submitted to it. The task then re-invokes itself, using the [Echo](/core/actions/echo) action to display the response status, content type, and body.

**Subscriptions**

```
mechanic/user/text
mechanic/actions/perform
```

**Code**

```liquid
{% if event.topic == "mechanic/user/text" %}
  {% action "http" %}
    {
      "method": "post",
      "url": "https://postman-echo.com/post",
      "body": {{ event.data | json }}
    }
  {% endaction %}
{% else %}
  {% action "echo",
    response_status: action.run.result.status,
    response_content_type: action.run.result.headers['content-type'][0],
    response_body: action.run.result.body %}
{% endif %}
```

## Related

* [Working with external APIs](/techniques/working-with-external-apis) — patterns for authenticating and integrating with third-party services
* [Responding to action results](/techniques/responding-to-action-results) — inspect HTTP responses and react to them in a follow-up task run
* [Preventing action loops](/techniques/preventing-action-loops) — guard against re-triggers when combining HTTP actions with event subscriptions


# Shopify

Run Shopify GraphQL mutations from Mechanic tasks — create orders, update products, manage customers, and more via the Shopify action.

The **Shopify** action sends requests to the [Shopify Admin API](https://shopify.dev/docs/admin-api). Use the [`shopify` Liquid filter](/platform/liquid/filters#shopify) to read data; use this action to write or update data. Use GraphQL for new Shopify automation work.

{% hint style="info" %}
In Mechanic, writing data to Shopify must happen using an action. While the Shopify action is usually the right choice, the [HTTP](/core/actions/http) action can also be used for this purpose, by manually configuring authentication headers. Reading data should use the [`shopify` Liquid filter](/platform/liquid/filters#shopify) unless you're running a bulk read that has to happen after the run.

To learn more, see [Interacting with Shopify](/core/shopify).
{% endhint %}

## Options

This action has several usage styles, each with a different set of constraints on action options.

### GraphQL

This usage style invokes the [Shopify GraphQL Admin API](https://shopify.dev/docs/admin-api/graphql). In this style, a single GraphQL query string is supplied as the action options. The [action](/platform/liquid/tags/action) tag has specific support for this action type, allowing this string to be provided as the contents of an action block.

{% hint style="info" %}
To prepare complex query inputs, use the [graphql\_arguments](/platform/liquid/filters#graphql_arguments) Liquid filter.
{% endhint %}

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "shopify" %}
  mutation {
    customerCreate(
      input: {
        email: "test@example.com"
      }
    ) {
      customer {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

### GraphQL with variables

This usage style invokes the [Shopify GraphQL Admin API](https://shopify.dev/docs/admin-api/graphql), and supports combining GraphQL queries with [GraphQL variables](https://graphql.org/learn/queries/#variables). This can be useful for re-using queries with multiple inputs, and is critical when dealing with very large pieces of input. Because GraphQL queries (excluding whitespace) are limited in length to 50k characters, GraphQL variables can be used in cases when large inputs (like Base64-encoded images) need to be submitted.

| Option      | Description                                              |
| ----------- | -------------------------------------------------------- |
| `query`     | Required; a string containing a GraphQL query            |
| `variables` | Required; a JSON object mapping variable names to values |

#### Basic example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% capture query %}
  mutation DeleteProduct($productId: ID!) {
    productDelete(
      input: {
        id: $productId
      }
    ) {
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

{% action "shopify" %}
  {
    "query": {{ query | json }},
    "variables": {
      "productId": "gid://shopify/Product/1234567890"
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}

#### Complex example

This example shows how the query and variables may be built up separately, and provided to the action using concise tag syntax.

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% assign metafield_owner_id = "gid://shopify/Customer/507332001849" %}
{% assign metafield_value = hash %}
{% assign metafield_value["foo"] = "bar" %}

{% capture query %}
  mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields {
        key
        namespace
        value
        createdAt
        updatedAt
      }
      userErrors {
        field
        message
        code
      }
    }
  }
{% endcapture %}

{% assign metafield = hash %}
{% assign metafield["ownerId"] = metafield_owner_id %}
{% assign metafield["namespace"] = "demo" %}
{% assign metafield["key"] = "demo" %}
{% assign metafield["type"] = "json" %}
{% assign metafield["value"] = metafield_value | json %}
{% assign metafields = array %}
{% assign metafields = metafields | push: metafield %}

{% assign variables = hash %}
{% assign variables["metafields"] = metafields %}

{% action "shopify" query: query, variables: variables %}
```

{% endtab %}
{% endtabs %}

## Bulk operations

The Shopify action can start Shopify bulk operations using `bulkOperationRunQuery` or `bulkOperationRunMutation`. Mechanic detects the returned bulk operation, monitors it, and re-invokes the same task with `mechanic/shopify/bulk_operation` when Shopify finishes.

Bulk operation mutations require a staged JSONL variables upload before calling `bulkOperationRunMutation`. Use the [HTTP action](/core/actions/http) for the multipart upload step, and use `mechanic/actions/perform` to move between the staged upload, file upload, and mutation-start steps.

* [Bulk operations](/core/shopify/bulk-operations)
* [Running bulk operation mutations](/resources/tutorials/bulk-operation-mutations)

## Related

* [Reading Shopify data](/core/shopify/read) — query products, orders, customers, and more using GraphQL in Liquid
* [Writing Shopify data](/core/shopify/write) — overview of writing data to Shopify
* [Responding to action results](/techniques/responding-to-action-results) — inspect Shopify mutation results in a follow-up task run
* [Preventing action loops](/techniques/preventing-action-loops) — guard against re-triggers when modifying resources you subscribe to


# Integrations

Connect Mechanic tasks to third-party services — Airtable, Google Sheets, Slack, Shopify Flow, and more.

These actions allow your Mechanic tasks to speak to other app and systems :) Use Mechanic to integrate your Shopify store with other apps like Airtable, Google, Slack, and more.


# Airtable

The Airtable action allows you to create and update tables and records in your Airtable bases.

It provides an authenticated client for HTTP calls to the [Airtable API](https://airtable.com/developers/web/api). The various Airtable API methods supported by this integration all share the same top-level structure in the action options.

## Options

<table><thead><tr><th width="182.33333333333334">Option</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>account</td><td>string</td><td>Required: the Airtable account to use. Must match one of the Airtable accounts linked in the Mechanic authentication settings.</td></tr><tr><td>method</td><td>string</td><td>Required: the HTTP verb as required by the Airtbale API for the specific method (e.g. "GET", "POST")</td></tr><tr><td>url_path</td><td>string</td><td>Required: the Airtbale API endpoint (e.g. "/v0/meta/bases/AIRTABLE_BASE_ID/tables")</td></tr><tr><td>headers</td><td>hash</td><td>Required: "Content-Type": "application/json"</td></tr><tr><td>body</td><td>hash</td><td>Required: the object that contains a JSON representation of the properties and content of the table, record, or fields being modified</td></tr></tbody></table>

### Supported API Permissions

This integration supports these Airtable API scopes.

* Read/Write Records
* Read/Write Comments
* Read/Write Base Schema

## Authentication

This action requires connecting an Airtable account with the appropriate permissions. To connect an account:

1. Go to **Settings → Authentication**
2. Select **Airtable** in the provider list
3. Follow the Airtable account connection flow

## Examples

### Create a New Table

```liquid
{% action "airtable" %}
  {
    "account": "AIRTABLE_ACCOUNT_NAME",
    "method": "POST",
    "url_path": "/v0/meta/bases/AIRTABLE_BASE_ID/tables",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "name": "Example Checklist",

      "fields": [
        {
          "name": "TODO",
          "type": "singleLineText"
        },
        {
          "name": "Complete",
          "type": "checkbox",
          "options": {
            "color": "greenBright",
            "icon": "check"
          }
        }
      ]
    }
  }
{% endaction %}
```

### Add Records to a Table

```liquid
{% action "airtable" %}
  {
    "account": "AIRTABLE_ACCOUNT_NAME",
    "method": "POST",
    "url_path": "/v0/meta/bases/AIRTABLE_BASE_ID/AIRTABLE_TABLE_ID",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "records": [
        {
          "fields": {
            "TODO": "Review Mechanic",
            "Complete": false
          }
        }
      ]
    }
  }
{% endaction %}
```

## Action Response

The body of the action response will vary based on which Airtable API method was called. Generally, the response is an object with the following structure (most fields removed for brevity). Running an Airtable action task and reviewing the response is often the best way to see what will be returned in the body.

```json
{
  "type": "airtable",
  "run": {
    "ok": true,
    "result": {
      "status": 200,
      "body": {
        ...
      }
    }
  }
}
```


# Flow

The **Flow** action sends data to Shopify Flow, arriving as one of four possible Flow triggers.

{% hint style="info" %}
This page is about the Mechanic action that sends data to Shopify Flow. For a review of Mechanic's entire integration with Flow, see [Shopify Flow](/platform/integrations/shopify-flow).
{% endhint %}

## Options

### Resource options

The Flow action accepts at most one resource option, identifying a specific Shopify resource, and resulting in a resource-specific Flow trigger. If no resource option is provided, Mechanic will use the General trigger.

{% hint style="warning" %}
These resource options only accept fully-numeric resource IDs (i.e. 12345). They do not accept global IDs (i.e. gid://shopify/Customer/12345).
{% endhint %}

| Resource option                    | Flow trigger                  |
| ---------------------------------- | ----------------------------- |
| `customer_id`                      | "Mechanic sent customer data" |
| `product_id`                       | "Mechanic sent product data"  |
| `order_id`                         | "Mechanic sent order data"    |
| (when no resource option is given) | "Mechanic sent general data"  |

### Data options

This action also sends user-defined data, with one option available for each of Flow's supported datatypes. These options are *always* sent to Flow, even if they're omitted from the action definition; when omitted, their values are set to the documented default.

<table><thead><tr><th width="198.0845070422535">Option</th><th width="150">Type</th><th>Default</th></tr></thead><tbody><tr><td><code>user_boolean</code></td><td>Boolean</td><td><code>false</code></td></tr><tr><td><code>user_email</code></td><td>Email address</td><td><code>"hey@mechanic.invalid"</code></td></tr><tr><td><code>user_number</code></td><td>Number</td><td><code>0</code></td></tr><tr><td><code>user_string</code></td><td>String</td><td><code>""</code></td></tr><tr><td><code>user_url</code></td><td>URL</td><td><code>"https://mechanic.invalid/"</code></td></tr></tbody></table>

## Usage

For a detailed review of usage, see [Shopify Flow](/platform/integrations/shopify-flow#mechanic-flow).


# Google

The Google action allows you to interact directly with the Google Drive and Sheets APIs, and is a more advanced integration type than the streamlined [Google Drive and Google Sheets](/platform/integrations/google-drive-and-google-sheets)actions. The advantage is that it allows use to use any of the features in the Google Drive, Sheets, and Docs REST API. Mechanic interacts with Google using OAuth2 for authentication.

{% hint style="warning" %}
Utilizing the Google action beyond the demonstration tasks requires reviewing and comprehending the [Google Drive API reference docs](https://developers.google.com/workspace/drive/api/reference/rest/v3). The Mechanic team will not be able to provide developer assistance with this API.
{% endhint %}

The Google action provides a wrapper around HTTP calls to the Google APIs. The various Google API endpoints supported by this integration all share the same top-level structure in the action options.

## Options

<table><thead><tr><th width="182.33333333333334">Option</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>account</td><td>string</td><td>Required: the Google account email address to authenticate with. Must match one of the Google accounts linked in the Mechanic authentication settings.</td></tr><tr><td>method</td><td>string</td><td>Required: the HTTP verb as required by the Google API for the specific method (e.g. "GET", "POST", "PATCH")</td></tr><tr><td>url_path</td><td>string</td><td>Required: the API endpoint for the relevant Google Drive service (e.g. "/drive/v3/files")</td></tr><tr><td>headers</td><td>hash</td><td><p>Required: different combinations of API endpoints and usage will dictate which headers are required. Review the Google API docs for more details.</p><p>Mechanic will use the following headers if they are missing and the request meets certain conditions.</p><pre class="language-json"><code class="lang-json">"Accept": "application/json",
"Content-Type": "application/json"
</code></pre></td></tr><tr><td>body</td><td>hash</td><td>Required: for creating/editing sheets and files, the object that contains a JSON representation of the properties and content of the file; for listing files, this optional object will contain the query parameters used for filtering, pagination, etc.</td></tr></tbody></table>

## Authentication

This action requires connecting a Google account with the appropriate Drive permissions. To connect an account:

1. Go to **Settings → Authentication**
2. Select **Google** in the provider list
3. Follow the Google account connection flow

## Examples

### Create Simple Text File

```liquid
{% action "google" %}
  {
    "account": "user@example.com",
    "method": "POST",
    "url_path": "/upload/drive/v3/files?uploadType=media",
    "headers": {
      "Accept": "application/json",    
      "Content-Type": "text/plain"
    },
    "body": "Hello world!"
  }
{% endaction %}
```

### Update Simple Text File

```liquid
{% action "google" %}
  {
    "account": "user@example.com",
    "method": "PATCH",
    "url_path": "/upload/drive/v3/files/{{ google_file_id }}?uploadType=media",
    "headers": {
      "Accept": "application/json",    
      "Content-Type": "text/plain"
    },
    "body": "Hello again world!"
  }
{% endaction %}
```

### List Drive Files with Name Filter

```liquid
{% action "google" %}
  {
    "account": "user@example.com",
    "method": "GET",
    "url_path": "/drive/v3/files",
    "headers": {
      "Accept": "application/json",
      "Content-Type": "application/json"      
    },
    "body": {
      "q": "name contains 'Mechanic'",
      "pageSize": 10,
      "fields": "files(id,name,mimeType,createdTime)",
      "trashed": false
    }
  }
{% endaction %}
```

### New Sheet with Data Rows

```liquid
{% action "google" %}
  {
    "account": "user@example.com",
    "method": "POST",
    "url_path": "/sheets/v4/spreadsheets",
    "headers": {
      "Accept": "application/json",
      "Content-Type": "application/json"
    },
    "body": {
      "properties": {
        "title": "Sheet File Title"
      },
      "sheets": [
        {
          "properties": {
            "title": "Sheet Tab Name"
          },
          "data": [
            {
              "rowData": [
                {
                  "values": [
                    {
                      "userEnteredValue": {
                        "stringValue": "Order name"
                      }
                    },
                    {
                      "userEnteredValue": {
                        "stringValue": "Date"
                      }
                    }
                  ]
                {
                  "values": [
                    {
                      "userEnteredValue": {
                        "stringValue": "#1357"
                      }
                    },
                    {
                      "userEnteredValue": {
                        "stringValue": "2025-10-05T12:00:00Z"
                      }
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  }
{% endaction %}
```

## Action Response

The body of the action response will vary based on which API endpoint is called. Generally, the response is an object with the following structure (most fields removed for brevity). Running a Google action task and reviewing the response is often the best way to see what will be returned in the body.

```json
{
  "type": "google",
  "run": {
    "ok": true,
    "result": {
      "status": 200,
      "body": {
        ...
      }
    }
  }
}
```


# Google Drive

The Google Drive action allows you to upload files to your Google Drive.

It supports various file types and can generate files dynamically using [file generators](/core/actions/file-generators), including text files, PDFs, CSVs, and HTML files. Mechanic interacts with Google Drive via the Google Drive API, using OAuth2 for authentication. \\

## Options

<table><thead><tr><th width="182.33333333333334">Option</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>account</td><td>string</td><td>Required: the Google account email address to authenticate with</td></tr><tr><td>uploads</td><td>hash</td><td>Required: a has specifying files to upload and their contents</td></tr></tbody></table>

### Uploads hash structure

The `uploads` hash supports these properties:

<table><thead><tr><th width="174">Property</th><th width="93">Type</th><th>Description</th></tr></thead><tbody><tr><td>overwrite</td><td>boolean</td><td>Optional: when true, files with matching names will be overwritten. Defaults to false</td></tr><tr><td>[path/filename]</td><td>string | hash</td><td>One or more file paths mapped to their content. Paths can include folders (e.g., 'reports/monthly/file.txt'). Content can be either a direct string or a <a href="/pages/-MXYcOuvq9zSw-QKgyRF">file generator object</a>.</td></tr></tbody></table>

## Authentication

This action requires connecting a Google account with the appropriate Drive permissions. To connect an account:

1. Go to **Settings → Authentication**
2. Select **Google** in the provider list
3. Follow the Google account connection flow

## Folder Support

Files can be organized in folders by including path information in the filename:

* Use forward slashes to separate folder names (e.g., "reports/2024/monthly/file.pdf")
* Folders will be created automatically if they don't exist
* Can only access folders created by this integration
* Invalid characters not allowed: `< > : " / \ | ? *`

### Path Examples

```
reports/monthly/report.pdf        # Three levels deep
data/2024/q1/sales.csv           # Four levels deep
archives/backups/files.zip        # Three levels deep
```

## Examples

### Simple Text File Upload

```liquid
{% action "google_drive" %}
  {
    "account": "user@example.com",
    "uploads": {
      "simple.txt": "Hello world!"
    }
  }
{% endaction %}
```

### Multiple Files with Overwrite

```liquid
{% action "google_drive" %}
  {
    "account": "user@example.com",
    "uploads": {
      "overwrite": true,
      "report.pdf": {
        "pdf": {
          "html": "<h1>Monthly Report</h1><p>This is a PDF generated from HTML</p>"
        }
      },
      "data.csv": "Date,Value\n2024-01-01,100"
    }
  }
{% endaction %}
```

### Files in Folders

```liquid
{% action "google_drive" %}
  {
    "account": "user@example.com",
    "uploads": {
      "overwrite": true,
      "reports/monthly/sales.pdf": {
        "pdf": {
          "html": "<h1>Monthly Sales Report</h1><p>Data for this month</p>"
        }
      },
      "data/exports/stats.csv": "Date,Value\n2024-01-01,100",
      "archive/backups/data.zip": {
        "zip": {
          "files": {
            "readme.txt": "Backup files",
            "data.csv": "id,value\n1,test"
          }
        }
      }
    }
  }
{% endaction %}
```

### Dynamic File Generation

```liquid
{% capture report_content %}
  <h1>{{ shop.name }} - Monthly Report</h1>
  <p>Generated on {{ "now" | date: "%Y-%m-%d" }}</p>
  <ul>
    {% for order in shop.orders %}
      <li>{{ order.name }}</li>
    {% endfor %}
  </ul>
{% endcapture %}

{% action "google_drive" %}
  {
    "account": {{ options.google_account | json }},
    "uploads": {
      "overwrite": true,
      "inventory-report.pdf": {
        "pdf": {
          "html": {{ report_content | strip | json }}
        }
      }
    }
  }
{% endaction %}
```

## Action Response

The action returns details about the uploaded files. The response is an object with the following structure:

```json
{
  "uploads": {
    [filepath: string]: {
      "id": string,          // Google Drive file ID
      "name": string,        // File name as stored in Drive
      "mime_type": string,   // MIME type of the uploaded file
      "web_view_link": string, // URL to view the file in Google Drive
      "path": string         // Full folder path where file was created
    }
  }
}
```

### Example response

```json
{
  "uploads": {
    "reports/monthly/report.pdf": {
      "id": "1ABC...xyz",
      "name": "report.pdf",
      "mime_type": "application/pdf",
      "web_view_link": "https://drive.google.com/file/d/1ABC...xyz/view",
      "path": "reports/monthly"
    }
  }
}
```


# Google Sheets

The Google Sheets action lets tasks create, read, write, and manage Google Sheets spreadsheets. Mechanic interacts with Google Sheets via the Google Sheets API, using OAuth2 for authentication — see [Authentication](#authentication).

{% hint style="info" %}
Due to Google security restrictions, Mechanic can only access spreadsheets that were created through Mechanic itself — no other spreadsheets in your drive. To work with Google Sheets:

* First create a spreadsheet using the `"create_spreadsheet"` operation
* Store the returned spreadsheet ID for later use
* Then use the other operations on this spreadsheet

See this great [example](https://tasks.mechanic.dev/demonstration-add-new-orders-to-google-sheet) in the task library.
{% endhint %}

## Operations

| Operation                                   | Use it to                                                   |
| ------------------------------------------- | ----------------------------------------------------------- |
| [`create_spreadsheet`](#create_spreadsheet) | Create a spreadsheet, with one or many named sheets         |
| [`append_rows`](#append_rows)               | Add rows to the end of a sheet's data                       |
| [`update_rows`](#update_rows)               | Overwrite rows at a specific range                          |
| [`clear_values`](#clear_values)             | Clear a range, or a whole sheet                             |
| [`get_values`](#get_values)                 | Read values into the action result                          |
| [`delete_rows`](#delete_rows)               | Delete a span of rows (rows below shift up)                 |
| [`add_sheet`](#add_sheet)                   | Add a sheet (tab) to an existing spreadsheet                |
| [`rename_sheet`](#rename_sheet)             | Rename a sheet (tab)                                        |
| [`delete_sheet`](#delete_sheet)             | Delete a sheet (tab)                                        |
| [`export_spreadsheet`](#export_spreadsheet) | Export the spreadsheet as XLSX, CSV, PDF, HTML, ODS, or TSV |

Every operation requires `"account"` (the email address of a [connected Google account](#authentication)), and every operation except `create_spreadsheet` requires `"spreadsheet_id"`. Each operation's remaining options are listed in its section below.

## Targeting sheets and ranges

These conventions apply across the operations:

* **`sheet_name`** targets a sheet (tab) by its exact name. Where it's optional (`append_rows`, `update_rows`, `clear_values`, `get_values`), omitting it targets the spreadsheet's **first sheet** — Mechanic looks up the actual name, so language-specific defaults ("Sheet1", "Foglio1", "Hoja1", …) are handled automatically. Pass plain names (e.g. `"Order Data"`); Mechanic quotes them for A1 notation as needed.
* **`sheet_range`** is an A1-notation range (e.g. `"Orders!A2:C10"`). A range without a sheet qualifier (e.g. `"A2:C10"`) combines with `sheet_name` when one is given. A qualified range that conflicts with `sheet_name` is rejected as an error.

## Writing values

Operations that write data (`create_spreadsheet`, `append_rows`, `update_rows`, `add_sheet`) accept `"value_input_option"`:

* `"raw"` (default) — values are stored exactly as given
* `"user_entered"` — values are parsed as if typed into the Sheets UI: dates become dates, numbers become numbers, and strings starting with `=` become formulas

{% hint style="warning" %}
**Only use `"user_entered"` with data you trust.** Under `user_entered`, any cell value beginning with `=` executes as a live formula in the spreadsheet. Customer-supplied values (order notes, names, line item properties) should be written with the default `"raw"` mode.
{% endhint %}

## Retries

* **Reads and idempotent writes** (`get_values`, `update_rows`, `clear_values`, `rename_sheet`) are retried automatically on transient errors.
* **Ambiguous writes** (`append_rows`, `create_spreadsheet`, `add_sheet`, `delete_rows`, `delete_sheet`) are not retried automatically, because a timeout may occur after Google already applied the change — retrying could duplicate rows or delete the wrong ones. `append_rows` supports opting in via `"retry_on_transient_errors": true`.
* **Rate limits** (HTTP 429) are always retried automatically, for every operation: Google rejects rate-limited requests before applying anything, so they're safe.

***

## create\_spreadsheet

Creates a new spreadsheet — with a single sheet (optionally populated via `rows`), or with multiple named sheets via `sheets`.

| Option               | Required | Notes                                                                                 |
| -------------------- | -------- | ------------------------------------------------------------------------------------- |
| `title`              | no       | Defaults to "New Spreadsheet"                                                         |
| `rows`               | no       | Array of arrays; initial data for the default sheet. Mutually exclusive with `sheets` |
| `sheets`             | no       | Array of `{"name": ..., "rows": [...]}` objects, to create multiple named sheets      |
| `folder_path`        | no       | Folder to create the spreadsheet in — see [Folders](#folders)                         |
| `value_input_option` | no       | See [Writing values](#writing-values)                                                 |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "create_spreadsheet",
    "title": "Monthly Sales Report",
    "rows": [
      ["Month", "Revenue", "Expenses", "Profit"],
      ["January", "5000", "3000", "2000"]
    ]
  }
{% endaction %}
```

With multiple sheets:

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "create_spreadsheet",
    "title": "Sales Report",
    "sheets": [
      { "name": "Orders", "rows": [["Order ID", "Total"]] },
      { "name": "Refunds", "rows": [["Refund ID", "Amount"]] }
    ]
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef",
  "title": "Sales Report",
  "folder_path": "reports/2026",
  "sheets": [
    { "name": "Orders", "sheet_id": 0 },
    { "name": "Refunds", "sheet_id": 1234 }
  ]
}
```

(`folder_path` is null when not given; `sheets` is present when created with `sheets`.)

## append\_rows

Adds new rows after a sheet's existing data.

| Option                      | Required | Notes                                                                       |
| --------------------------- | -------- | --------------------------------------------------------------------------- |
| `rows`                      | yes      | Array of arrays                                                             |
| `sheet_name`                | no       | Defaults to the first sheet — see [Targeting](#targeting-sheets-and-ranges) |
| `sheet_range`               | no       | An explicit A1 range to append within                                       |
| `value_input_option`        | no       | See [Writing values](#writing-values)                                       |
| `retry_on_transient_errors` | no       | Defaults to false — see [Retries](#retries)                                 |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "append_rows",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Orders",
    "rows": [
      ["Order ID", "Customer", "Total"],
      ["1001", "John Doe", "99.99"]
    ]
  }
{% endaction %}
```

Building rows dynamically:

```liquid
{% assign order_rows = array %}

{% for order in shop.orders %}
  {% assign order_row = array %}
  {% assign order_row[0] = order.name %}
  {% assign order_row[1] = order.customer.name %}
  {% assign order_row[2] = order.total_price %}
  {% assign order_rows[order_rows.size] = order_row %}
{% endfor %}

{% action "google_sheets" %}
  {
    "account": {{ options.google_account | json }},
    "operation": "append_rows",
    "spreadsheet_id": {{ options.spreadsheet_id | json }},
    "rows": {{ order_rows | json }}
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "updated_range": "Orders!A1:C3",
  "updated_rows": 3,
  "updated_columns": 3,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## update\_rows

Writes rows at a specific range, overwriting whatever is there.

| Option               | Required         | Notes                                    |
| -------------------- | ---------------- | ---------------------------------------- |
| `rows`               | yes              | Array of arrays                          |
| `sheet_range`        | one of these two | e.g. `"Orders!A5:C5"`                    |
| `sheet_name`         | one of these two | Alone, writes starting at the sheet's A1 |
| `value_input_option` | no               | See [Writing values](#writing-values)    |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "update_rows",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_range": "Orders!A5:C5",
    "rows": [["1001", "John Doe", "129.99"]]
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "updated_range": "Orders!A5:C5",
  "updated_rows": 1,
  "updated_columns": 3,
  "updated_cells": 3,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## clear\_values

Clears the values in a range; formatting is left intact.

| Option        | Required         | Notes                          |
| ------------- | ---------------- | ------------------------------ |
| `sheet_range` | one of these two | The range to clear             |
| `sheet_name`  | one of these two | Alone, clears the entire sheet |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "clear_values",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_range": "Orders!A2:C100"
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "cleared_range": "Orders!A2:C100",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## get\_values

Reads values from a range into the action's result. Results are capped at 20MB; narrow the range for very large sheets.

| Option                | Required | Notes                                                                                              |
| --------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `sheet_range`         | no       | Defaults to the entire first sheet when neither this nor `sheet_name` is given                     |
| `sheet_name`          | no       | Alone, reads the entire named sheet                                                                |
| `value_render_option` | no       | `"formatted_value"` (default), `"unformatted_value"` (raw numbers), or `"formula"` (cell formulas) |

Pair it with a `mechanic/actions/perform` subscription to use the values in a task — for example, finding which row holds a particular order before updating or deleting it:

{% code title="Task subscriptions" %}

```liquid
mechanic/user/trigger
mechanic/actions/perform
```

{% endcode %}

{% code title="Task code" %}

```liquid
{% if event.topic == "mechanic/user/trigger" %}
  {% action "google_sheets" %}
    {
      "account": "user@example.com",
      "operation": "get_values",
      "spreadsheet_id": "1234567890abcdef",
      "sheet_range": "Orders!A1:C100"
    }
  {% endaction %}
{% elsif event.topic == "mechanic/actions/perform" %}
  {% assign sheet_rows = action.run.result.values %}
  {% action "echo" sheet_rows %}
{% endif %}
```

{% endcode %}

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "range": "Orders!A1:C100",
  "values": [["Order ID", "Customer", "Total"], ["1001", "John Doe", "99.99"]],
  "row_count": 2,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## delete\_rows

Deletes a span of rows entirely — rows below shift up. Row numbers are 1-based and inclusive, matching what you see in the Sheets UI.

{% hint style="warning" %}
This operation is destructive, and is never retried automatically — see [Retries](#retries). `sheet_name` is always required, so a reordering of tabs can never redirect a deletion to the wrong sheet.
{% endhint %}

| Option       | Required | Notes                                          |
| ------------ | -------- | ---------------------------------------------- |
| `sheet_name` | yes      |                                                |
| `start_row`  | yes      | 1-based                                        |
| `end_row`    | no       | Defaults to `start_row` (deletes a single row) |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "delete_rows",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Orders",
    "start_row": 5,
    "end_row": 7
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "sheet_name": "Orders",
  "start_row": 5,
  "end_row": 7,
  "deleted_rows": 3,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## add\_sheet

Adds a new sheet (tab) to an existing spreadsheet, optionally populated with initial rows.

| Option               | Required | Notes                                 |
| -------------------- | -------- | ------------------------------------- |
| `sheet_name`         | yes      | Must not already exist                |
| `rows`               | no       | Initial data for the new sheet        |
| `value_input_option` | no       | See [Writing values](#writing-values) |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "add_sheet",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Q3",
    "rows": [["Date", "Revenue"]]
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "sheet_id": 5678,
  "sheet_name": "Q3",
  "updated_rows": 1,
  "updated_columns": 2,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

(`updated_rows`/`updated_columns` are present when `rows` were written.)

## rename\_sheet

Renames a sheet (tab), located by its current name.

| Option           | Required | Notes                  |
| ---------------- | -------- | ---------------------- |
| `sheet_name`     | yes      | The current name       |
| `new_sheet_name` | yes      | Must not already exist |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "rename_sheet",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Q3",
    "new_sheet_name": "Q3 Archive"
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "sheet_id": 5678,
  "previous_sheet_name": "Q3",
  "sheet_name": "Q3 Archive",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## delete\_sheet

Deletes a sheet (tab) by name. A spreadsheet's last remaining sheet cannot be deleted. Like `delete_rows`, this is never retried automatically — see [Retries](#retries).

| Option       | Required |
| ------------ | -------- |
| `sheet_name` | yes      |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "delete_sheet",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Q3 Archive"
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "sheet_id": 5678,
  "sheet_name": "Q3 Archive",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}
```

## export\_spreadsheet

Exports a spreadsheet. The exported file arrives base64-encoded in the action result.

| Option      | Required | Notes                                                            |
| ----------- | -------- | ---------------------------------------------------------------- |
| `file_type` | no       | `"xlsx"` (default), `"csv"`, `"pdf"`, `"html"`, `"ods"`, `"tsv"` |

```liquid
{% action "google_sheets" %}
  {
    "account": "user@example.com",
    "operation": "export_spreadsheet",
    "spreadsheet_id": "1234567890abcdef",
    "file_type": "pdf"
  }
{% endaction %}
```

Response:

```json
{
  "spreadsheet_id": "1234567890abcdef",
  "name": "Monthly Sales Report",
  "size": 12345,
  "file_type": "pdf",
  "data_base64": "base64encodeddata..."
}
```

***

## Authentication

This action requires connecting a Google account with the appropriate permissions. To connect an account:

1. Go to **Settings → Authentication**
2. Select **Google** in the provider list
3. Follow the Google account connection flow

## Folders

When creating spreadsheets, use `folder_path` to organize your files:

* Use forward slashes to separate folder names (e.g., `"reports/2026/monthly"`)
* Folders will be created if they don't exist
* Mechanic can only access folders created by this integration
* Invalid characters not allowed: `< > : " / \ | ? *`


# Report Toaster

Report Toaster is a reporting and analytics app, which offers an integration with Mechanic. Use the Report Toaster action to perform operations with their service.

## Documentation

Find a complete reference for this action in the Integrations section:

→ [Platform / Integrations / Report Toaster / Action](/platform/integrations/report-toaster#action)


# Slack

The Slack action allows Mechanic to post messages to public and private channels in your Slack instance (as the Mechanic app bot or a customer username).

The Slack action provides a wrapper around HTTP calls to the [Slack Web API](https://docs.slack.dev/apis/web-api/). The various Slack API methods supported by this integration all share the same top-level structure in the action options.

## Options

<table><thead><tr><th width="182.33333333333334">Option</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>account</td><td>string</td><td>Required: the Slack account to use. Must match one of the Slack accounts linked in the Mechanic authentication settings.</td></tr><tr><td>method</td><td>string</td><td>Required: the HTTP verb as required by the Slack API for the specific method (e.g. "GET", "POST")</td></tr><tr><td>url_path</td><td>string</td><td>Required: the Slack API method (e.g. "/chat.postMessage")</td></tr><tr><td>headers</td><td>hash</td><td>Required: "Content-Type": "application/json"</td></tr><tr><td>body</td><td>hash</td><td>Required: the object that contains a JSON representation of the properties and content of the message</td></tr></tbody></table>

### Supported API Methods

Currently, these are the only Slack API methods supported by this integration.

* [chat.postMessage](https://docs.slack.dev/reference/methods/chat.postmessage)
* [chat.update](https://docs.slack.dev/reference/methods/chat.update/)
* [chat.delete](https://docs.slack.dev/reference/methods/chat.delete/)

## Authentication

This action requires installing the Mechanic Slack app in your Slack account with the appropriate permissions. To install the app:

1. Go to **Settings → Authentication**
2. Select **Slack** in the provider list
3. Install the Mechanic Slack app

## Examples

### Post a Simple Message

```liquid
{% action "slack" %}
  {
    "account": "SLACK_ACCOUNT_NAME",
    "method": "POST",
    "url_path": "/chat.postMessage",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "channel": "CHANNEL_ID",
      "text": "Slack Example Message",
      "blocks": [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": "Lorem ipsum dolor sit amet consectetur adipiscing elit."
          }
        }
      ]
    }
  }
{% endaction %}
```

### Post a Message with Custom Username and Icon

```liquid
{% action "slack" %}
  {
    "account": "SLACK_ACCOUNT_NAME",
    "method": "POST",
    "url_path": "/chat.postMessage",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "username": "AiRobot",
      "icon_emoji": ":robot_face:",
      "channel": "CHANNEL_ID",
      "text": "Slack Example Message",
      "blocks": [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": "Lorem ipsum dolor sit amet consectetur adipiscing elit."
          }
        }
      ]
    }
  }
{% endaction %}
```

## Action Response

The body of the action response will vary based on which Slack API method is called. Generally, the response is an object with the following structure (most fields removed for brevity). Running a Slack action task and reviewing the response is often the best way to see what will be returned in the body.

```json
{
  "type": "slack",
  "run": {
    "ok": true,
    "result": {
      "status": 200,
      "body": {
        ...
      }
    }
  }
}
```


# File generators

Generate files from Mechanic tasks — PDF, CSV, ZIP, plaintext, and base64 formats for attachments and downloads.

**File generators** are invoked by [**actions**](/core/actions) to create new files, using options provided by the action, and handing the resulting file back to the action for further use. In this way, [**tasks**](/core/tasks) can make choices about what files to generate, and what to do with the results.

| File generator                                       | Purpose                                                                                |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [Base64](/core/actions/file-generators/base64)       | Decodes base64-encoded content, returning a file containing the results                |
| [PDF](/core/actions/file-generators/pdf)             | Renders HTML using a full Webkit browser, returning a PDF file of the results          |
| [Plaintext](/core/actions/file-generators/plaintext) | Allows defining file contents using a plain string, instead of a file generator object |
| [URL](/core/actions/file-generators/url)             | Downloads and returns a file                                                           |
| [ZIP](/core/actions/file-generators/zip)             | Accepts its own set of file generators, returning a ZIP archive of the results         |

## Maximum filesize

Generated files may each be a maximum of 20MB.

{% hint style="info" %}
"But why?", you may well ask!

Mechanic allows action run results to be fed back into the system ([via mechanic/actions/perform](/techniques/responding-to-action-results)). File generators usually end up having their resulting files represented in the action run results, and base64-encoding 20mb of binary data makes for a lot of JSON. We have to draw a line somewhere. :person\_shrugging:
{% endhint %}

## Object structure

File generator objects, like [action objects](/core/tasks/code/action-objects), are plain JSON objects each having a single key, and a single value. The object key specifies which file generator is to be invoked; the object value contains the options used for that generator.

```
{
  FILE_GENERATOR_TYPE: FILE_GENERATOR_OPTIONS
}
```

In practice, file generator objects are given as values in a larger JSON object, in which filenames are mapped to file generators.

{% hint style="info" %}
The [plaintext](/core/actions/file-generators/plaintext) file generator is invoked implicitly by supplying a string, instead of supplying the usual file generator object.
{% endhint %}

In the following example, a [Files](/core/actions/files) action is defined, mapping filenames (`"invoice.pdf"`, `"external.jpg"`, and `plain.txt`) to file generators (a PDF generator, a URL generator, and – implicitly – a plaintext generator). Note how the file generator invocation varies, based on the specific file generator in play.

```liquid
{% action "files" %}
  {
    "invoice.pdf": {
      "pdf": {
        "html": "<h1>Order #12345</h1>\n<p>It's due!</p>"
      }
    },
    "external.jpg": {
      "url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"
    },
    "plain.txt": "This\nis\na\nmulti-line\nplaintext\nfile."
  }
{% endaction %}
```

## Supported actions

These are the Mechanic actions that support file generators.

| Action                       | Usage                                                                                            |
| ---------------------------- | ------------------------------------------------------------------------------------------------ |
| [Email](/core/actions/email) | Uses file generators to prepare email attachments                                                |
| [Files](/core/actions/files) | Uses file generators to prepare temporary URLs, from which the generated files can be downloaded |
| [FTP](/core/actions/ftp)     | Uses file generators to prepare FTP uploads                                                      |
| [HTTP](/core/actions/http)   | Adds generated files to a multipart/form-data HTTP request                                       |


# Base64

The **Base64** file generator accepts a base64-encoded string, and returns a file containing the decoded value.

This generator is useful when producing images, or other binary content that cannot be represented with a JSON string.

## Options

This file generator accepts a base64-encoded string. It does not support any other options.

```json
{
  "base64": BASE64_ENCODED_VALUE
}
```

## Example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "files" %}
  {
    "image_from_base64.jpg": {
      "base64": "iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAIAAAA3GddeAAAAAXNSR0IArs4c6QAAAMZlWElmTU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAExAAIAAAAVAAAAZodpAAQAAAABAAAAfAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciBQcm8gMi4wLjUAAAAEkAQAAgAAABQAAACyoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAvoAMABAAAAAEAAAAuAAAAADIwMjE6MDI6MTMgMDA6MjQ6MjMAdW0xkQAAAAlwSFlzAAALEwAACxMBAJqcGAAAA6ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjQ2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjQ3PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAyMS0wMi0xM1QwMDoyNTozMlo8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDIxLTAyLTEzVDAwOjI0OjIzWjwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciBQcm8gMi4wLjU8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Ci1W4c0AAA3XSURBVFgJvZgLlBTVmcfvraquqn5Uv3t63swwvAaQhyBCFEUhWSIgKJrVaBLMakjMaszRzWp213PUPScn8WCOiWZfybpn17hZ4yMcgkogQRAIEeMwwwwzTGaaGaZ7evpR1V3vd929zfCKYCKIqalTdaf6Vt1f/b/vft93CyKEwF92mxwRD4v/IIB4eIgbkIAQUH8xEtd1MYGoqNnx0gQviZolmZ6DSB9FMK7d2V63YE47/KS1QZ7nep6uKAd+e3DCIL26GQbB8aIhqYaHgJ9lY9GQI0t7tr7x2H03foLa4PfEevDlcm/vEcmwIqnGkCTnxNKITY0Jmmu7sbA/GAgwrE8uGSqk9nRnPikarIdUrRw90qs5Xrx5mqdbuUIJ09GVYVvIOOwsKhCLxsLJOEN5riRJuod4zbn8NFgS27KO9fYUe/rS+SIcHFaFSiQZLyxamudCFEnNTaNGd7wST6Xr2VSMtDWEIFIcD3vwZaaxXUcoFo+/s4/evoPv7n61UBzUdSBW7qfoldnRHWs2GO2t8UikI0CpThbE6Za6eEkhVcvBTp0IMZeTxrSszOiIuWMn8drWN3304IpV42I1mxk+NpLpEip7+/oWRaK9M/8mEg1Hg35WUxPW0RTRyXuxYlVRda0xGbxsNLqm9Q0PE4d+X319667m1lLHjInenpH+o6VigZIlwTGPREI3TORiJJmMheMB1vDBplhjodDff2JKviS6ut6WDl8eGlVRuvuOQt20f7F1pz+UiSXHDuwfGTpWEQRHV13bhJ4bmDOb7JwRi3FxLhhnGH8sAiBhT3BH3n6jUCDrOaKzrZH4+NFPqlb7eo9QiQbY1d2Tyx1FoLe7+w/H+gWet3XVcy3kua0U3UmRTnNLvC4VphkCeLph5Eu57EQpaefc0uDSzob6VPTj0miqMtDfJxpOvijoY2OQZU9MFMZOjEhi1TFU5Fg49gUI4t5INH3wXTkQCkXCELi6rZXF0nBmZGBwCJKgzs6sWjwdR+qPZSnL8Y4ODEyUymRiytDo2NyqNK0qRqqyqaqWrgLHwpG+EYKNtP8BwzCvW06uWkGSSFWrkiwXiuXR0Wy5zOdGs/UR6qZrr/hYeUoXxYFMNqMiQAbzx4+XNOuIR6xy0bcp8JJnHsa+AlAHQHcQ5AbbUzo7+Ucf4RpSmlQRxaogVPKFMt4qvFAu8UuWX5+IRy893khDQ++/+tpxLtlx35e7fn2kwGcJCvQ3T/XaJm4sZ//eVIuKGEZgBkVRyVRh8eLKI98ML5hriIJYxZtU4rF/YxK+zAs47N1960aIlbmE6OeaZvW9Q13PPNNblNgnfvh+n5FRQjpRRxaPEIFg//KVQu54c6ouJlfTPrqnvh7deAO3dnUkHDL5iWqlIklqpSoKlSqPWUp8qVS+/dbbprZNmZxMH+o3nuPYqkoFgwRJ4q4eTsWGbubGlXf2Dv/ouf5sceiuh5vDU0d6h0tVRTJCpBFtI/XW5nq6s8MNrHODrJmIhNPJZMhPq5JaHMcGEiVFFJUaDTZQGaMIsVjyjttOCfOh2iDblrZtBcf+QKaSyO93cU1kGk6hwL+9Rzq4f8xxD0xbEpz9qbHhMTxFFdMyEUmEp+fN0TpDb6GTiXAwGeNi4WDAs5GgiKokYQpZFWUFE9VsVBNGkCTl0Ye/GgoFJ4X5UBplz2/Q4/8YE0Sg6bptYWWok3WaAdAJ5B1iwtmORVMJ/9jICUkzHRzFcMUU4FT/zLwxOs3hKY8lkd8HbEvHEUCWFTyHNFnRREmqVKqlmoH4iWJp+bJrVq5YTsCzUeYCljL27hGffLK5yANNBbbtdxw/AiYAPIRjPjIDicP+lB5Ojxd5QVJcQOLqjSZJhkTRWBL6EhZ812ceZrzZrhlTagQqTkKKakiSjG2EVTlpIz4WS9x7z5cgcRblAtpYPYetpx4PZjLAVIFrI89xCWAhoABQgrV9GNKj/gTnkkVR9jxEUaSPJFmaigbZdMiXrquzyGCI3toaPFpUm3keSLKjGmbNTBJGqdmoXBZwDfHA5q80NTZMTqULW8oaHXG2fAceHySwFqQDKBej4BmIaUQEBIB4hAaooM0lbToIMApB0D4ywPgiQbYxxtUnw+EIhdz67tJVSHzNB3mp2s5LHpZGwR5TxfOoUuYrmqrfe8+ma69ZSvyxMH+kDVJU+4XnggN785yb9tvAjxDOMA5wbKC6QHCA6IBxjxz2J5lUCxWJkyRGoTiWiYcD9YlQSyrMhJjA2LAiFI8psE/pmEJ0UaRrgYgoy9hhxarE45ktSretv+X2Detx2XVGkjON05cQ0v5jS/D4y9Z0M6whAq8uNOSZwDWBaQLBqO2SDoeNoBhpapoyLRTFVRwZDvpTUa4uxiWiIQvo9PZXPv3icwc75v520aqiy3YrnSjf3cplfHQQuzCOwbblPPT1+29Zt5Y8GTXOQJxpnKIx3tnNFF4BHZ5XdVkHL3RspLuugVMtQC5w8FEFRhllDIaJ1SebGiKRIDYQViXOBTg/qUoF+o3/Xbn9pQlA74635DXLNGQ82xSvrjw61MBUAcAFKnzwa19dv+azH4aCmWo0niSj/d+nZslAcykWkYgENgAGIDCNRStNn07NWcEoRqlrzNqVSTWlW5vSYY4LsTTHUAQ0J04MCr/55eZfvSr7ueev/EwPV+9VRRunSdO0sH1Bsjp+bE57cvOmuzes+SxOFWeUOL9R+03b9XKg/RBgAfLbRADTUcBioekh09WpBem7nvUlGk/s/52Y743NZJMds1obkgwBKeCYSik/1Jvfte3a6jg3fdb3mJYDbJyoVBzLsm3HclzTsnVNX71s2YNfvPnqRQv/NAomoayREUp/lrjSBDqEtgstAEwPKBTQ/cCizI7HuPjUf3vhv7f//KcIEnmTnb9iTcJPKVW+WBzPHT4o7Hlzjiqsvf66f3VjOyouEEXoeXgaqh4yTJuzlIduX/WljX81vb3lfCXOv0KZwzsCy2wvuAR5cUcPWLLgZ2wIcYB15fyi9JI1g8cGfvLcD13OVwVJcbT/dztfZ1auLo70Fw7uM4+8m66Ub0nGy9nyzyAlewRlOQ4kZBexmrJam/jCt75+w603RSPh8we+4BXKv+AzZAyjhCBkfB5F2CZBupBEgEBcS52P9hGF3SuWWz/uvTLKmKEw+/7ebWJuqFES6KGjrGlcTdPNJPXUhJwNx0IEiR2eNfV5Kv95MTv/a5sWbryJDn9UFMxHUYl2fJrMFbgoJHxnodVcF1n+wdT52XsaWrqeVrMZbfE1n5L5UveuX6U0g3LsBkBd5wv0+rhtLt1Ynmgx5FlGdYktX28b8IpZ9bdvvCiUGs3Zwc9pWZrK7342MnOInmlRkaZOLrDlQbwyCemgQzDkDtOIOnZzhLxlKRNhfW/9WprvVJe5+ixkT0dOGnkNCOTWr2dmzDjnkR+peSEaz60e+kk8+RIz9UbgxoDNUMhZOAfGYs6L/7Lv/e17E6aTIoi1Abh4Stuhtqktv3xzletOIVEE4HlZ2yskwc2bxwbPlgofieV8bVzHKRzelRSfp+e3ARQCLgeAD6A8pOOUKmp794WFSh1FfG4eMXdaM3/vMy3ZPMlsa9ABU+sHav6GQ0TA749GP5ARPwrQ6YSOkCzkygNv4Szp9v8D3VwGTCtuApgAMALo4Pig+j9/+wv5PWF2ANy1iZ13TyN66KnUwpVjo+N1QRjApTrAfn/yCAFB4ix0IdX/HNGpe3AuIA1e79+Xyww3hLtRFC88NODSgJABVaerjQd+/PPht8fqI8TabycW33wVTG2pwLp//8536372n7OkmiCTqhD40xke0rZcyzpnPvw5itO/n3kDSPhTnr+lgX+MaPaAPwk8XEGwgOpAIJjZe2j/j/bXp8i7n5/afn2bJP9TLmPtfPq+pX37p5/ga7EJnqLBj8VfPsKa7pRKp4e4iPMpGvxCyLIYc5ReSKHo1TXFIQdgDNCWMmHu/O5/+UP2ff93VXpeR2n45oaF88HYM5vsN4K4IsZZFZvGq0lychECsNWk2XOYjmkXQXG662m/gdC1Hd/4bsBGYCAOXPz4IKChbRx875UXdC33wK7FjQupyuC8his24CDHhWCi3gIsgj5AYpqTKFiVEAIT626mXvwpvWDB6SEu4nxKG1uTlC231SV6HQGHYhqm5wCS8ign18Vne3s2vzw7wkqV7rXxhZsphsHFIGRjAK9tWJf0gcn6EH9pxQlX/Nxfx554kpk+/ZRQF0FS63qKhvTRxrQkcfUUTx2HrU3AqABhyPbVa4q9/omE3y0U37szvfpxAhegeO4AGnDYySBigIlTOcT/1x5kfeOhwMOP+BobLw3lLA0uUWmkAZpEWcsb+T0CjN1TpqmB2XPicjcoiBsjq/5uEqX2CnijA26AgqzDsICjMJPf/f4PQnfcQeCIN+k+k90u8nhSG4SKPfsb5C47F3D7PVCRfC1Bs0yPg9W+5htCM69tbJhJ0DjGnto8XLnrRQoi6Ac+PyhNabW/9c+Nn78TJ73TXS7xXLu/ViQaJbgcUjBAzDBhiCoOrYt85emoIfJHX0+3zvvg63qGp+QpktCCAeGmO8ObH022teN11SUinHPbpDZetfewRdxKzFhNpVTIcPEvrqMCIZ+TYOMPfBAF03uWLYuyNcu6/3tN16zAHJeQBM5hONusfdlHjlM++FZ8yUrSd9ocf9L2nmtq+UGko9A0/AmoFnsv1/b/1t5SZGHoKlMAAAAASUVORK5CYII="
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# PDF

Generate PDF files from HTML in Mechanic tasks — invoices, packing slips, receipts, and reports with modern CSS and web fonts.

The **PDF** file generator accepts an object containing an HTML string, and uses [Pdfcrowd](https://pdfcrowd.com/) to render it as a PDF document. Pdfcrowd employs the [Chromium Embedded Framework](https://en.wikipedia.org/wiki/Chromium_Embedded_Framework) for HTML rendering, which uses the same foundation as Google Chrome. This allows Mechanic to generate PDFs with modern CSS and JavaScript features, including chart libraries and web fonts.

## Options

<table data-header-hidden><thead><tr><th width="145">Option</th><th>Description</th></tr></thead><tbody><tr><td>Option</td><td>Description</td></tr><tr><td><code>html</code></td><td>Required; a string containing the HTML, CSS and JavaScript to be rendered</td></tr><tr><td>...</td><td>Additional Pdfcrowd API options supported; see below</td></tr></tbody></table>

```json
{
  "pdf": {
    "html": HTML,
    ...
  }
}
```

## Pdfcrowd options

The PDF generator supports all rendering-related options of the Pdfcrowd API, using version 20.10.

For a complete list of options, see <https://pdfcrowd.com/doc/api/html-to-pdf/http/>.

### Debugging

If it's unclear why something isn't rendering properly, start by testing the HTML being used in a Pdfcrowd playground, at <https://pdfcrowd.com/playground/html-to-pdf>. If the issue is reproducible in the playground, use the "Help" button along the left-hand sidebar to get the ID of your specific playground, and instructions for contacting Pdfcrowd support with the details of your test.

{% embed url="<https://www.loom.com/share/94ea5776e2d84b6d92c1e7ca551e6e92>" %}
A screencast illustrating an HTML test, and a path for reaching Pdfcrowd support
{% endembed %}

## Example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% capture html %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Liu+Jian+Mao+Cao&display=swap" rel="stylesheet">
<style>p { font-family: 'Liu Jian Mao Cao', cursive; }</style>

<p>Almost before we knew it, we had left the ground.</p>
<div id="tester" style="width:100%;height:40vh;"></div>

<script src="https://cdn.plot.ly/plotly-2.2.0.min.js"></script>
<script>
  // from https://plotly.com/javascript/getting-started/
  TESTER = document.getElementById('tester');
	Plotly.newPlot( TESTER, [{
	x: [1, 2, 3, 4, 5],
	y: [1, 2, 4, 8, 16] }], {
	margin: { t: 0 } } );
</script>
{% endcapture %}

{% action "files" %}
  {
    "file.pdf": {
      "pdf": {
        "html": {{ html | json }},
        "page_width": "7in",
        "page_height": "5in",
        "margin_top": "10mm",
        "margin_right": "10mm",
        "margin_bottom": "10mm",
        "margin_left": "10mm"
      }
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# Plaintext

The **plaintext** file generator is used implicitly, when a JSON string is given in place of a standard file generator JSON object. The resulting file will contain the content of the string, with no further processing. This makes the plaintext generator suitable for text files, CSV files, TSV files, and any other file format that can be expressed using plain text.

The plaintext generator cannot be invoked explicitly; `"plaintext"` cannot be used as a named generator type.

## Options

Because this file generator is used implicitly, when a string is given instead of a file generator object, this file generator does not use options.

## Example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "files" %}
  {
    "plain.txt": "This\nis\na\nmulti-line\nplaintext\nfile."
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# URL

The **URL** file generator accepts a string as its options, containing a valid URL. This generator downloads the file at that URL, returning the results.

Downloaded files may be a maximum of 20 megabytes, even when used within other file generators (like [ZIP](/core/actions/file-generators/zip)).

## Options

This file generator accepts a string containing a valid HTTP or HTTPS URL. It does not support any other options.

```json
{
  "url": URL
}
```

## Example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "files" %}
  {
    "image_from_url.png": {
      "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# ZIP

The **ZIP** file generator accepts an options object, specifying a set of files (themselves defined using file generators) to be compressed into a single ZIP file. The resulting ZIP file may optionally be password-protected.

## Options

| Option     | Description                                                                 |
| ---------- | --------------------------------------------------------------------------- |
| `files`    | Required; an object specifying a set of filenames mapped to file generators |
| `password` | Optional; a string specifying a password to use for encrypting the file     |

```json
{
  "zip": {
    "files": FILENAMES_AND_FILE_GENERATORS,
    "password": PASSWORD
  }
}
```

## Example

{% tabs %}
{% tab title="Liquid" %}

```liquid
{% action "files" %}
  {
    "secure.zip": {
      "zip": {
        "password": "opensesame",
        "files": {
          "confirmations.txt": "this data is protected with zipcrypto encryption",
          "image.png": {
            "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
          },
          "receipt.pdf": {
            "pdf": {
              "html": "<h1>!!</h1>"
            }
          }
        }
      }
    }
  }
{% endaction %}
```

{% endtab %}
{% endtabs %}


# Runs

Mechanic processes Shopify automations as runs — event runs, task runs, and action runs move through queues for reliable execution.

When something happens in your store, Mechanic processes it as a series of runs. First it handles the event, then runs the matching tasks, then performs the resulting actions. You can see this activity on the [Events page](/app/events) in the app — each entry shows the runs involved.

[Events](/core/events), [tasks](/core/tasks), and [actions](/core/actions) are all processed using queues, in which a piece of work is enqueued, and performed in its turn. Each piece of work is called a **run**. Thus, Mechanic performs work using event runs, task runs, and action runs.

When performed, a run has a **result**. Depending on the type of run, this result may define additional runs to be performed after it concludes.

* **Event runs**, when performed, may result in a set of enqueued task runs.
* **Task runs**, when performed, may result in a set of enqueued action runs.
* **Action runs**, when performed, have behaviors that vary by [action type](/core/actions#action-types).
  * If the originating task [subscribes to mechanic/actions/perform](/techniques/responding-to-action-results), most action runs will spawn a new event containing that action's results. This new event will be processed in an enqueued event run, creating an opportunity for the task to respond to the action's results. Echo actions do not emit these events, and any action can opt out by setting `__perform_event` to `false`.

Most runs are scheduled to be performed immediately. Some runs may be [scheduled](/core/runs/scheduling) for the future. Some runs may be [retried](/core/runs/retries), once performed.

At the moment a run is performed, it loads in all related data (which may include the related store, or the related event, or the related task).

## Run flow

A normal flow in Mechanic looks like this:

1. An event is created – possibly by a [Shopify webhook](/core/shopify/events), by a [Mechanic webhook](/platform/webhooks), by the [Mechanic scheduler](/platform/events/topics#scheduler), or by an [Event action](/core/actions/event).
2. An event run is created, and performed. During this phase, Mechanic scans the store's tasks to see which ones are relevant for the current event, by checking the subscriptions on file for each task. For each task that Mechanic discovers for the event, a task run is created. (If the task subscription involved an [offset](/core/tasks/subscriptions#offsets), as in mechanic/scheduler/daily+2.hours, the task run will be set to wait for that amount of time.) The result of the event run is this set of task runs.
3. Each task run is performed. During this phase, Mechanic takes each task's [Liquid code](/core/tasks/code), and renders it using the associated event. The result of the task run is the set of JSON [action objects](/core/tasks/code/action-objects) rendered by the task's Liquid code. Each action object is used to create an action run.
4. Each action run is performed. During this phase, Mechanic executes each action, given the options that were provided for it by the task run's result.

**Understanding this sequence of events is important.** Task runs do not come into existence until the event run has been performed, and action runs are only performed after their task run has fully concluded.

Critically, this means that tasks do not have direct access to the effects of the actions they generate. Actions are performed later in the sequence, and their effects will only be seen by subsequent task runs.

## Run priorities

In general, given a mix of event, task, and action runs that are all due, Mechanic will perform due action runs first, then due task runs, and finally due event runs.

If Shopify's [rate limit](/core/shopify/api-rate-limit) for *either* the GraphQL or REST Admin API has been reached, Mechanic will skip over task runs and over [Shopify action](/core/actions/shopify) runs, until *both* rate limits have been recovered. In these cases, Mechanic may choose to perform due runs of a lower priority, while it waits for the Shopify API rate limits to recover sufficiently to perform the higher priority runs.

## Run states

<table data-header-hidden><thead><tr><th width="216">Run state</th><th>Description</th></tr></thead><tbody><tr><td>Unscheduled</td><td>The run has not been assigned a time to be performed</td></tr><tr><td>Scheduled</td><td>Scheduled to be performed, but that time has not yet arrived</td></tr><tr><td>Due</td><td>The run is ready to be performed, and is waiting for a runner</td></tr><tr><td>Started</td><td>The run is being performed</td></tr><tr><td>Failed</td><td>The run has been performed, and an error has been recorded</td></tr><tr><td>Succeeded</td><td>The run has been performed, without errors</td></tr></tbody></table>


# Scheduling

[Event](/core/actions/event) and [task](/core/tasks) runs may be **scheduled** to perform in the future. They will not have any effect until they are performed. This means that their eventual performance may be impacted by changes to a store's Mechanic account, prior to the scheduled performance time.

## Event runs

### The Event action

Event runs may be scheduled using the [Event](/core/actions/event) action, using its `run_at` option to define the time at which the run should be performed.

The task runs that arise from a scheduled event run will not be established until the event run is performed. (This does not apply if the `task_ids` option is used, which determines ahead of time which tasks may be run in response to the new event.) This means that changes to the set of enabled tasks can have an impact on what tasks are actually run, in response to a scheduled event run.

### Scheduler events

Mechanic supports several [scheduler topics](/platform/events/topics#scheduler) (such as mechanic/scheduler/hourly), allowing tasks to be automatically invoked by the platform on a regular repeating interval.

Event runs generated in response to scheduler events are always adjusted for the store's local time.

## Task runs

### Subscription offsets

Task runs may be scheduled using [subscription offsets](/core/tasks/subscriptions#offsets), in which a task states that it wishes to run later (by some amount of time) than the event that triggers it.

Subscription offsets are a property of the task, and are applied by the task run – *not* the event run. This means that the subscribed-to event must be created and run *before* the subscription offset is calculated and applied.

{% hint style="info" %}
In some cases, the first task run on a new mechanic/scheduler/daily task may not be performed when expected.

To illustrate: if a user creates a task at 9am Monday, subscribing to mechanic/scheduler/daily+10.hours, they will have to wait until *the following midnight* before the mechanic/scheduler/daily event is created. When that event's run is performed, the task's subscription offset will be calculated and applied, and the task run will be enqueued for 10 hours later. This means that the task will run for the first time on 10am Tuesday, *not* 10am Monday.
{% endhint %}

### The Event action

To achieve precise scheduling (e.g. "run on December 16th at 2:30pm"), or to accomplish scheduling for an interval not supported by Mechanic's scheduler topics, use the [Event](/core/actions/event) action to schedule an event run at any chosen time, with a [custom event topic](/platform/events/topics#user). Make sure that the desired task is subscribed to the same custom topic, and consider using the Event action's `task_id` option to specify that *only* the desired task is allowed to respond to the new event.

Task runs that are scheduled for the future will always use a task's latest configuration, including the task's [options](/core/tasks/options), [code](/core/tasks/code), and [Shopify API version](/core/tasks/shopify-api-version).

If a task is disabled or deleted at the time a task run comes due, the task run will still perform at the scheduled time, but will fail instantly.


# Concurrency

In general, Mechanic will process as many [**runs**](/core/runs) simultaneously as possible. This means that multiple tasks subscribing to the same event topic are very likely to execute simultaneously, when such an event occurs.

To protect the health of the system and to ensure performance for every store on the platform, Mechanic have several **concurrency** limits, defining the conditions in which Mechanic will perform runs simultaneously.

{% hint style="info" %}
In most cases, an inefficient run queue is best addressed by combining or reorganizing tasks, improving [Shopify API usage efficiency](/core/shopify/api-rate-limit) (converting REST requests to GraphQL is often helpful), or by making judicious use of [event filters](/platform/events/filters).

It can be also useful to temporarily disable a task responsible for the backup; doing so will cause Mechanic to instantly fail its enqueued runs when they come up for processing, but it will not fail those task runs ahead of time.
{% endhint %}

## Limits

Each store's Mechanic account has a fixed run queue size. This limit controls how many runs Mechanic will perform simultaneously for your store. With a limit of 2, this could mean 2 events, or 2 tasks, or 1 event and 1 tasks and 0 actions, or any other combination of runs. Additional runs will be performed as the preceding runs complete.

{% hint style="info" %}
Related FAQ: [Can my Mechanic concurrency limit be raised?](/faq/can-my-mechanic-concurrency-limit-be-raised)
{% endhint %}

## Tips

* Use GraphQL to query Shopify, to keep your data usage efficient. (To learn more, see [Interacting with Shopify](/core/shopify).)
* For options for ordering execution of runs, see [Ordering](/core/runs/ordering).


# Ordering

In general, Mechanic's [**run**](/core/runs) system does not guarantee the execution order for runs that have been created at the same time (see [Concurrency](/core/runs/concurrency)). This applies to all kinds of runs: events, tasks, and actions.

For tasks, the simplest way to manage this is by using subscription delays, offsetting the time at which each task is run. For example, if you have two tasks that subscribe to shopify/customers/create, you might adjust one so that it it subscribes to shopify/customers/create+10.minutes instead. This way, your first task has a chance to execute and run before the other.

This is not a perfect solution: naturally, if the first task takes more than 10 minutes to run, there will still be overlap. So, Mechanic makes

## Guaranteeing run order for actions

Each task has an advanced option called "[Perform action runs in sequence](/core/tasks/advanced-settings/perform-action-runs-in-sequence)". When this is enabled, all generated actions for a given task run will be executed precisely in order.

## Guaranteeing run order for tasks

The best tool to leverage here is the [Event action](/core/actions/event), coupled with action sequences (see above).

1. Begin by making a list of the tasks for which you need to guarantee run order, sorted by the desired run order. For these purposes, all of these tasks should subscribe to the same event topic.
2. Beginning with the task that should run first, (a) enable "Perform action runs in sequence", and (b) add an "event" action at the very end of your task code. The intent here is for this action to kick off a unique event topic that the *second* task should then subscribe to.
3. Having added that "event" action, update the second task so that it subscribes to your new event topic, *instead of* the original event topic. If there is a third task that should follow this one, repeat step 2 for this task as well, in preparation for kicking off the third task.
4. Repeat until you reach the final task in your list. This task does not need an "event" action at its conclusion; it only needs to have its subscription updated to listen for the penultimate task's generated event.

One more tool is worth mentioning: tasks may subscribe to mechanic/actions/perform to be re-triggered when each of their own actions are performed. For more on this strategy, see [Responding to action results](/techniques/responding-to-action-results).


# Retries

In some cases, a run that has already been performed may be performed again, using a **retry**.

When a run is retried, its previous result is permanently discarded. Because of this, runs that already have a meaningful result (i.e. an event run that gave rise to task runs, or a task run that generated actions, or an action run that succeeded) cannot be retried.

Runs are given **automatic retries** when a non-permanent error is encountered. In some cases, Mechanic permits **manual retries** for runs, allowing users to reset a run's result and perform the run again.

## Retry context

Retried event runs will always reflect Mechanic's current configuration, including any [event filters](/platform/events/filters).

Retried task runs will always use a task's latest configuration, including the task's [options](/core/tasks/options), [code](/core/tasks/code), and [Shopify API version](/core/tasks/shopify-api-version).

Retried action runs will always use their original action options, as dictated by the task run that generated them. Action runs are entirely unaffected by updates to their task.

{% hint style="info" %}
Outstanding task and action runs that belong to a newly-disabled task will always fail when performed, whether they're retried or performed normally. This means that disabling a task – as long as it remains disabled – ensures that it will not perform any work, even if it has task or action runs already scheduled.
{% endhint %}

## Automatic retries

When non-permanent errors are encountered, Mechanic will automatically retry a run. For [HTTP actions](/core/actions/http), this might be a connection error. For [Email actions](/core/actions/email), this might be a temporary outage with our email provider.

Mechanic will automatically retry these runs up to 4 times, for a total of 5 attempts. Retries are subject to a variable backoff delay, of approximately 0:30, 1:16, 2:32, and 5:08 respectively, for each of the 4 retries.

## Manual retries

Some task runs may be manually retried, via the Mechanic user interface.

### Task runs

Task runs may be retried...

* ... if the task run itself failed (due to a Liquid error, an API error while reading data, or something else)
* ... or, if the task run did not generate any actions

During task development, it can be useful to set up a task to only render [log objects](/core/tasks/code/log-objects). A task run which only rendered log objects can be retried, and this ability to retry can be convenient when rapidly iterating on task code.

![This example was generated from a task whose code contained only a {% error "Oh no!" %} tag.](/files/w38uJ31UbU8h4kWZXrWy)

### Action runs

Only failed action runs may be retried.

![This example was generated from a task whose code contained only a {% action "echo", \_\_error: "Oh no!" %} tag.](/files/feX7ncofaTC8BuD0EE1Y)


# Reading and Writing to Shopify

How Mechanic Shopify automation tasks read and write store data — inline GraphQL queries for reads, and Shopify actions for mutations.

Mechanic tasks have full access to the Shopify Admin API. Tasks can **read** data inline during execution — running GraphQL queries to fetch additional context beyond the triggering event — and **write** data via actions that run Shopify GraphQL mutations after the task completes. This gives tasks the same level of API access available to custom apps, without requiring you to build or host app infrastructure.

## Reading data

Use the [`shopify` Liquid filter](/platform/liquid/filters#shopify) to query Shopify data inline during a task run. This filter accepts a GraphQL query and returns the result immediately.

```liquid
{% capture query %}
  query {
    product(id: "gid://shopify/Product/1234567890") {
      title
      status
    }
  }
{% endcapture %}

{% assign result = query | shopify %}

{{ result.data.product.title }}
```

Learn more: [Reading data](/core/shopify/read)

## Writing data

Use the [Shopify action](/core/actions/shopify) to create, update, or delete Shopify resources. Shopify actions are queued during the task run and **performed after the task code finishes**, so you cannot use the result of a mutation in the same task run that creates it.

```liquid
{% action "shopify" %}
  mutation {
    productUpdate(input: { id: "gid://shopify/Product/1234567890", title: "New Title" }) {
      product {
        title
      }
      userErrors {
        field
        message
      }
    }
  }
{% endaction %}
```

Learn more: [Writing data](/core/shopify/write)

## Bulk operations

Use [bulk operations](/core/shopify/bulk-operations) when Shopify should process a large read or write asynchronously. Bulk operation queries read large datasets into JSONL results; bulk operation mutations run the same mutation many times using a staged JSONL variables file.

{% hint style="warning" %}
Shopify REST is deprecated in Mechanic. Use the GraphQL Admin API for all new tasks. See [Converting tasks from Shopify REST to GraphQL](/resources/converting-tasks-from-shopify-rest-to-graphql) for migration guidance.
{% endhint %}

## Responding to Shopify events

Mechanic can respond to Shopify webhooks (like `shopify/orders/create` or `shopify/products/update`) by subscribing tasks to the corresponding [event topics](/core/events/topics).

Learn more: [Responding to events](/core/shopify/events)

## Related

* [API rate limit](/core/shopify/api-rate-limit) — how Mechanic manages Shopify's rate limits
* [API versions](/core/shopify/api-versions) — how tasks select a Shopify API version
* [Bulk operations](/core/shopify/bulk-operations) — asynchronous Shopify reads and writes
* [Shopify admin action links](/core/shopify/admin-action-links)




---

[Next Page](/llms-full.txt/1)

