Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 252 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Mechanic

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Core Concepts

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Hire a Mechanic 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. This is a growing list of established developers, both independent and agency, who can help you with your implementation.

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 and see if they can help you!

"I need something custom!"

Great! Mechanic is made for this. Here's where to start. :)

Mechanic is a development platform – in the hands of a developer, it can be used to accomplish almost anything in Shopify. While our 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.

Need something that you think others might need too? The Mechanic community accepts , and the top-voted requests are regularly selected for implementation.

If you're using AI...

Tread lightly! We've got notes on this here: .

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 . 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, start by picking something from our that's close to what you're looking for, then modify it as needed.

Make sure to take advantage of the documentation found here, beginning with the section. Mechanic is a powerful system, and grounding yourself in the fundamentals is a good way to begin.

Finally, join Mechanic's Slack workspace at to exchange support with the community. The #general channel is a great place to start, and it's filled with people who are using Mechanic to solve problems every day. :)

Shopify is deprecating the REST API

Shopify is evolving its platform to enhance performance and provide more powerful features. As part of this evolution, Shopify has announced the deprecation of the Shopify Admin REST API. In response, we are updating our services to align with this change, transitioning fully to the GraphQL Admin API.

What Is Changing?

  • Deprecation of REST Admin API: Starting October 1, 2024, the REST Admin API is considered a legacy API. Shopify will begin phasing it out, with critical endpoints like product and variant endpoints ceasing to function on February 1, 2025. While product and variant endpoints are the first to be deprecated, Shopify plans to deprecate all REST endpoints in the future.

  • GraphQL Admin API: All new developments and updates will utilize the GraphQL Admin API exclusively. This shift is aimed at leveraging the enhanced capabilities and efficiencies that GraphQL offers.

Why is Shopify Making this Change?

Shopify wants to maintain one API and have decided to discontinue the REST Admin API in favor of the GraphQL Admin API. GraphQL addresses several limitations of REST, offering significant benefits such as:

  • Efficient Data Retrieval: Fetch precisely the data you need in a single request.

  • Enhanced Flexibility: Customize queries to suit your specific needs.

How Does This Affect You?

  • Library Tasks Update: All our library tasks will be updated to use GraphQL only. You will get a notification via email and in app if there is an update available for a task you have installed. Tasks that can be auto-updated will be updated for you. These updated tasks will serve as examples to help you migrate your custom tasks.

  • Custom Tasks Migration: If you have custom tasks relying on the REST API, they will need to be migrated to use the GraphQL API before the deprecation deadlines. See our guide .

  • Action Required: To ensure uninterrupted service, please begin updating your custom tasks to GraphQL as soon as possible.

Specific Impacts

After February 1, 2025:

  • Product and Variant REST liquid objects will cease functioning

  • for Product and Variants will now pass resource IDs instead of full REST objects - tasks will need to look up objects using these IDs

  • REST lookups in email task options will no longer work

  • Any tasks using REST API for product/variant operations will stop working

We recommend migrating all tasks to GraphQL now, not just those affected by the February deadline, as all REST endpoints will eventually be deprecated.

Next Steps

  1. Review Our Migration Guides: We have prepared comprehensive guides to assist you in migrating your custom tasks to GraphQL. Access them .

  2. Update Tasks from the Library: You will get a notification via email and in app if there is an update available for a task you have installed. Tasks that can be auto-updated will be updated for you.

  3. Learn More About GraphQL: Familiarize yourself with GraphQL by visiting Shopify's official .

Getting Help

  • Hire a Partner: Browse our for expert help

  • Community Support: Join our to ask questions about migrating your tasks

Task library

Mechanic's task library is a compendium of e-commerce automation tasks and documentation, written by the Mechanic community and the Mechanic core team. , everything is open-sourced under the highly permissive , making all library tasks appropriate for re-use and modification.

When building a new task, it's often easier to modify an existing task than to create a task from scratch. Searching GitHub is a good place to start, when looking for inspiration.

To browse the task library, visit .

The Mechanic community can request new tasks – see .

The task library is open for contributions, by way of pull requests – see .

Send an email when a specific product is shipped

task library
task requests
"I need help with my AI-written task!"
partners.mechanic.dev
task library
Core Concepts
slack.mechanic.dev
here
Admin Links
here
migration guide
partner directory
Slack community
Hosted on GitHub
MIT license
tasks.mechanic.dev
Requesting
Contributing

Introduction

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

Mechanic is a Shopify development and automation platform.

  • Search the task library for an off-the-shelf solution

  • Learn about "going custom" for a task that fits you perfectly

  • Ask in our community Slack workspace if you need something else

Find Mechanic on the Shopify App Store: apps.shopify.com/mechanic

Can Mechanic help me?

Mechanic is a Shopify development and automation platform, which comes with a rich library of pre-written automation tasks – and our users write their own custom tasks every day. Let's find out if Mechanic might work for you.

  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?

    • We have hundreds of common scenarios already handled with pre-written, open-source, modifiable, off-the-shelf tasks.

  3. As far as your problem concerns Shopify data, is what you want to do supported by the Shopify Admin API?

    • Mechanic's toolkit goes beyond Shopify's APIs, but a Mechanic task can only interact with Shopify data in ways that Shopify supports.

  4. Are you open to going custom?

    • Whether you need a developer or already have that covered, the path to creating a custom Mechanic task is well-established.

That list wasn't exactly a formal flowchart, but we hope it's helpful as you're evaluating Mechanic for your purposes. At its best, Mechanic is a platform and toolkit that you go to, and return to, when you hit the limits of the Shopify admin. And it's a community that collectively has learned how to solve many, many kinds of problems. (Join our community Slack workspace!)

Got a question you need answered now? Join our Slack workspace. 💬

How does Mechanic work?

Tasks, events, and actions

A developer writes tasks – Mechanic's term for a piece of automation. These tasks can respond to many different events, like a Shopify webhook, a manual trigger, a regular interval (e.g. hourly, daily), or an incoming email. Tasks use subscriptions to signal their interest in specific event types.

When a task receives an incoming event, it can choose to generate an action – an operation that has an effect.

  • The Shopify action makes changes to a Shopify store, like tagging, publishing, creating or deleting resources. It provides direct and complete access to Shopify's admin API, with support for both REST and GraphQL.

  • The Email action is for sending email. It supports custom templates, and attachments.

  • The 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 action performs any request, to any HTTP endpoint. This facilitates integration with third-party APIs.

  • The 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.

Liquid

Mechanic makes heavy use of Liquid – a template language created by Shopify. Its primary use is in task code. 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, our Liquid implementation includes additional support for constructing arrays and hashes, and includes many useful filters, making data processing more efficient.

Run queues

Mechanic performs work using queues of 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 and the Shopify API rate limit.

Slack community

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

Got some code to share in Slack? Use 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!

Events

In Mechanic, an event represents anything that happens. This could be an order being paid, or a customer record being created, or a fulfillment being delivered.

An event always has a topic, and data (even if the data is null/nil). Event attributes may be referenced in Liquid using the Event object.

Events may trigger any number of tasks, resulting in any number of actions.

Events are fed into Mechanic by the responsible party – for events that are about things in Shopify, for example, the events come to Mechanic from Shopify itself.

Incoming events may be selectively skipped using event filters.

Interacting with Shopify

Integrations

Auto-tag products when their SKU(s) change

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

Auto-tag orders with their tracking numbers

Adding an optional time delay to your Mechanic task

Auto-publish new products

"I need help with my custom task!"

What support is included with my Mechanic subscription?

Our support team assists with platform issues and issues with tasks from . For custom tasks not included in our task library, our support service is limited to Mechanic platform support.

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: .

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 to find a developer who can customize or optimize your task. If you want to be matched with a suitable developer, use our .

"I need help with my AI-written task!"

AI thrives on good examples. Our task library — — is full of good examples.

And if you get too stuck, hire a human. :) We've got those at .


Hey there! :) I'm Isaac, the creator of Mechanic. Those two lines above are the most important things to know. Keep reading if you're curious.

AI makes it super easy to create code. This is awesome. I'm so, so excited about this. The more the merrier.

This means Mechanic needs to learn something too: how to work with AI coders that can easily produce well-formed code but might not understand the patterns that Mechanic itself strictly abides by.

An AI can rapidly produce code, and it'll be good-looking code, but if the AI doesn't understand Mechanic the code might not work at all. In a very real way, it becomes a case of Mechanic not understanding the AI.

This is an interesting bind: because AI changes faster than Mechanic, it becomes a question of how best to guide the AI. For folks who understand code less than the AI, this puts everyone in a rough position: the AI is doing its best but can't tell when it doesn't understand, and the human is doing their best but can't tell when the AI doesn't understand, and all Mechanic knows is that it's being given something it doesn't understand.

As of this writing (currently June 12, 2025), AI is getting better at Mechanic. It's definitely getting better. But, still:

  • AI code often "invents" Mechanic features that do not exist (like writing task code in YAML, or compiling action objects into a JSON array)

  • AI code often fails to invoke necessary Mechanic features (like !).

  • Intelligence is a game of guessing intelligently: AI often sort of just guesses at what task code is supposed to generate.

Mechanic, as a platform, is a place for solving things together. "Together" works best when everyone's honest about where they're at. Mechanic's a good place for that. :)

The AI path with Mechanic is getting better, but it's not smooth yet. If you're having trouble with this, head to — that page has an overview of the smooth paths that do exist.

To learn about how we at Lightward Inc roll with AI, please visit , and say hello. :)

No matter what: thank you for being here. ❤️

=Isaac

User Form

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 that have the _userform flag.

When submitted, an event is generated, to which only this task will respond. The event contains the user's input in its data, making user's input available in event.data.

Click the link button beside the title of the form to copy a link to the form that you can share with your users

Getting the user’s input in code

During a mechanic/user/form event, the ad-hoc values arrive under event.data.

Tip: event.data contains the values for the fields you exposed; fall back to options.* for everything else.

Import and export

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 .

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.

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.

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.

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.

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 if the option does not meet its criteria.

A modification to a task option will always result in a new 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 is used to generate an . The error message will then be shown to the user, and they will be prevented from saving the task until they provide valid input.

Reconciling missing events

Shopify does not offer a strict guarantee on webhook delivery. In rare cases (and usually in high-volume situations), we've observed Shopify fail to send a webhook.

Quoting from for this scenario:

Your app shouldn't rely solely on receiving data from Shopify webhooks. Because webhook delivery isn't always guaranteed, you should implement reconciliation jobs to periodically fetch data from Shopify.

This applies to Mechanic tasks as well (which are, essentially, tiny apps).

For tasks that respond to events on Shopify resources, we recommend the following, using shopify/orders/create as an example:

  1. Update the task code to mark orders as having been processed. This could take the form of an order tag (e.g. "processed-by-task-xyz"), or a metafield. Additionally, ensure that this code skips orders that are already marked as processed.

  2. Add a , like mechanic/scheduler/15min. Then, update the task code so that these scheduled runs are used to scan for and process new orders in the last 15 minutes that have not yet been processed. This is the reconciliation step, ensuring that all new orders are ultimately processed, one way or another.

Example

Mechanic task library
Mechanic Slack Community
partners.mechanic.dev
matchmaking service
tasks.mechanic.dev
partners.mechanic.dev
subscriptions
learn.mechanic.dev/custom
lightward.ai
{% 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 %}
error object
preview
error tag
error object
{% # 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 event data, we don't want the value from the task option %}
  {% assign big_event = event.data.the_big_event__date_userform %}
  {% assign color_for_big_event = event.data.color_for_big_event__color_required_userform %}

{% endif %}

{% action "echo" big_event, level, color_for_big_event %}
task options
User form on Run Task page
contributing to the task library

Code

A task's code is a 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, logs, and errors.

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

Task code always has access to a set of environment variables, which can be used to make decisions about what JSON objects to render.

A task must purposefully consider its preview, so as to accurately communicate its intent to users and to the Mechanic platform.

To find many examples of task code, browse https://github.com/lightward/mechanic-tasks.

Responding to events

Shopify uses webhooks to notify apps like Mechanic about new activity. Mechanic supports every type of Shopify webhook in its set of Shopify event topics. By setting up subscriptions to these topics, a task may respond to any supported type of Shopify activity.

Note that Shopify does not strictly guarantee webhook delivery. See Reconciling missing events for more on this subject.

Responding to changes in specific data

Shopify's "update" webhooks do not contain information about what piece of data has changed. (For example, a product update webhook does not specify what attribute of the product has changed.) For this reason, it's not possible to subscribe to changes in specific resource attributes (like product SKUs, or order tags).

If a task needs to react to a specific attribute change, the task must scan for and "remember" the original value of that attribute, so as to compare incoming updates with that remembered value. A task could use the Cache action to store these values in the Mechanic cache, or it could use the Shopify action to save the remembered value in a metafield.

For an example implementation, see the Auto-tag products when their variants change task.

Tutorials

Video walkthroughs

Shopify's recommendation
Mechanic scheduler subscription

Delete all orders

JavaScript

Shopify allows apps to inject JavaScript into the online storefront. (This is facilitated by ScriptTag in the Shopify API.)

Mechanic supports this by allowing each task to specify its own JavaScript, to be injected into the online storefront.

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.

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; 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.

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 generates a new child event, when performed

  • A subscription to the mechanic/actions/perform 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.

Example

mechanic/user/trigger
user/fan/out
{% 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.

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.

Retried task runs will always use a task's latest configuration, including the task's options, code, and 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.

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.

Automatic retries

When non-permanent errors are encountered, Mechanic will automatically retry a run. For HTTP actions, this might be a connection error. For Email actions, 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. 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.

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.

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).

Options

This file generator accepts a string containing a valid HTTP or HTTPS URL. It does not support any other options.

{
  "url": URL
}

Example

{
  "action": {
    "type": "files",
    "options": {
      "image_from_url.png": {
        "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
      }
    }
  }
}

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

{
  "action": {
    "type": "files",
    "options": {
      "plain.txt": "This\nis\na\nmulti-line\nplaintext\nfile."
    }
  }
}

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

Tip: The data you generate can be imported directly into Google Sheets. Learn more:

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 to upload your feed to your own server.

Instructions

1. Create your task.

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

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 script, 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.

Next, click "Add a new template".

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).

Next, fill in the template contents with the following:

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

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".

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 .

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.


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 is often an important step to ensure that Mechanic prompts for the correct scopes for reading and writing Shopify API data.

To assist with generating a paginated query block, you can use the in the Mechanic code editor, and it will prompt you to choose the object type to paginate over (e.g. products).

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 . This shortcut will no longer be accessible for product and variant REST resources once they are fully deprecated.

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.

Tasks

In Mechanic, a task is a bundle of logic and configuration, that responds to and interprets . The result of a task can define , which are the task's opportunities to have an effect on the world.

A task responds to events based on its . When an event is received that matches a subscription, the task processes the event using its . The code has access to the event data; it also has access to the user's task configuration, through . Task code is written in Liquid, and is responsible for rendering a series of JSON objects (including , , and objects), defining work to be performed once task rendering is complete.

A task uses its 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 a task requires.

Tasks may be written from scratch, or installed from the Mechanic library (available in-app and ). Once installed, a task's code may be modified at any time.

Working on getting better at task-writing? See , and .

Example

This very basic task subscribes to shopify/customers/create, and renders an , using an email subject and body taken from user-configured .

Subscriptions

A task subscription is the expression of a task's intent to receive certain , filtering by . 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.

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 .)

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 .

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.

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.

The 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:

Remember, Mechanic does not permit access to the Shopify API during . Using this unless statement ensures that reloading only happens during a live event.

Using Liquid

A task's subscriptions are parsed for Liquid, at the time the task is saved. Combined with , 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

Ordering

In general, Mechanic's system does not guarantee the execution order for runs that have been created at the same time (see ). 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 "". 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 , 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 script. The intent here is for this action to kick off a unique event topic that the second task should the 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 .

Concurrency

In general, Mechanic will process as many 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.

In most cases, an inefficient run queue is best addressed by combining or reorganizing tasks, improving (converting REST requests to GraphQL is often helpful), or by making judicious use of .

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.

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.

Related FAQ:

Tips

  • Use GraphQL to query Shopify, to keep your data usage efficient. (To learn more, see .)

  • For options for ordering execution of runs, see .

Upgrading a Mechanic task: Adding a time delay

Sync inventory for shared SKUs

REST - Looping through a product's collections
{% 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 %}
GraphQL - Querying a product's collections with pagination
{% 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 %}
preview block
"paginated_query" snippet
REST - product metafield value check
{% assign metafield = product.metafields.custom.my_field %}

{% if metafield.value == "Alpha" %}
  {% log "metafield value matched" %}
{% endif %}
GraphQL - product query with metafield and value check
{% 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 %}
dot notation
run
Concurrency
Perform action runs in sequence
Event action
Responding to action results
runs
Shopify API usage efficiency
event filters
Can my Mechanic concurrency limit be raised?
Interacting with Shopify
Ordering
{%- layout none -%}
{{- shop.metafields.mechanic.feed -}}
.
Can I send data to Google Sheets?
FTP action
Task: Create a product inventory feed
our community Slack
{% unless event.preview %}
  {% assign customer = customer.reload %}
{% endunless %}
shopify/orders/create

{% if options.send_email_when_order_cancelled__boolean %}
  shopify/orders/cancelled
{% endif %}
shopify/orders/paid+{{ options.days_to_wait_before_followup__number_required }}.days
shopify/customers/create{% if options.wait_one_hour__boolean %}+1.hour{% endif %}
events
topic
Event action
Scheduling
Shopify variables
event preview
task options

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).

REST - shop.products resource loop
{% 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 %}

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 ... %}.

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.

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 docs

If a query has the potential to return a very large number of resources (including connected resources) in a shop, then a bulk operation query may be better suited than using paginated GraphQL queries.

GraphQL - paginated products query
{% 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 %}

To assist with generating a paginated query block, you can use the "paginated_query" snippet in the Mechanic code editor, and it will prompt you to choose the object type to paginate over (e.g. products).

To see a code diff from a Mechanic library task that was recently converted in this manner, click here.

Scheduling

Event and task 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 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 (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, 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.

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.

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 action to schedule an event run at any chosen time, with a custom event topic. 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, code, and 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.

Topics

To make events easy to identify, each event has a topic. Tasks signal their interest in specific event topics using 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".

Looking for an index of event topics? Start here.

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 can be used with any User event topic

  • Webhooks may be configured to generate events using any User event topic

The Shopify action

The Shopify action allows developers to submit any request to the Shopify Admin API. By responding to action results, the data returned by Shopify can be retrieved, and re-used by the calling task.

This approach should (probably) only be used as a last resort, when Mechanic's other methods of reading data do not cover the scenario.

events
actions
subscriptions
code
options
action
error
log
preview
on GitHub
Practicing writing tasks
Writing a high-quality task
Email action
options
shopify/customers/create
{"name":"Customer signup alerts","options":{"email_recipient__email_required":"[email protected]","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":[]}

Contributing

Mechanic's task library is a central resource for the entire community, and is continually enriched through contributions, via pull requests on GitHub.

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, and we'll be glad to help.

The task library is hosted in a Git repository on GitHub. You'll make your contribution via a Pull Request.

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 the task library repository.

    • 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, 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.

  2. Visit the task library repository and fork it as shown below. You'll make your changes to this copy of the repository.

  3. The task library is made up of the tasks and the supporting documentation. In these next few steps, you'll ensure you can build the 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!

  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.

  6. If you're changing an existing task you export 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 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. The CLA assistant 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.

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 and actions you need.

This is a tutorial for getting started quickly. To learn more about webhooks themselves, see Mechanic webhooks.

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, click the "Settings" button in the upper-right corner, then navigate to the "Webhooks" tab.

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

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.

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

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

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

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

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.)

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):

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

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.

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

{
  "zip": {
    "files": FILENAMES_AND_FILE_GENERATORS,
    "password": PASSWORD
  }
}

Example

{
  "action": {
    "type": "files",
    "options": {
      "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>"
              }
            }
          }
        }
      }
    }
  }
}

API versions

The Shopify API supports versioning for their REST and GraphQL admin APIs. (Learn more: shopify.dev/api/usage/versioning.)

Each Mechanic task has an individually-configured Shopify API version, defaulting to the latest stable version at the time of the task's creation. A task's version will apply to all Shopify API calls generated by its task code, in addition to all calls performed by that task's actions.

Each stable Shopify version is supported for one year. 30 days before a version ends support, tasks on that version will be silently upgraded to the next stable version. As a consequence, versions that are unsupported (or are within 30 days of no longer being supported) are not available in Mechanic.

Task usage

Deprecations

Every quarter, Shopify releases a new version of the Admin API, and simultaneously removes the oldest version of the admin API. (Subsequent calls to removed APIs will be responded to by the oldest still-supported version.)

As Shopify prepares to pull support for specific API calls, deprecations are announced, and are communicated in API responses.

Learn more about Shopify's deprecation practices

Deprecations are ignored for the latest stable version of the Shopify API, i.e. the version most recently released. Tasks whose runtime settings are configured for the latest stable version will not be flagged for deprecations, even if they use deprecated Shopify API features.

Identifying deprecations

If support for a task's Shopify API version will be pulled soon, its deprecations will be shown above the main task list.

Deprecation details are available in the advanced task editor, in the Runtime tab.

Resolving deprecation warnings

Deprecation warnings can be dismissed by doing one of the following:

  • Selecting a new Shopify API version for the task

  • Updating the task script

  • Disabling the task

  • Deleting the task

API rate limit

Mechanic has native awareness of Shopify's Admin API rate limit, and will accordingly manage the execution of operations that require access to the Shopify API. Mechanic users do not need to manage the API rate limit themselves.

If the rate limit has been reached, any due task runs or Shopify action runs will wait to be enqueued until the rate limit has recovered.

If the rate limit is reached during a run's performance, Mechanic will automatically wait and retry any affected API queries until they succeed, up to a certain number of retries. If the API rate limit does not recover in a reasonable amount of time, Mechanic will raise a permanent error for the run.

Learn more about the Admin API rate limit from Shopify, at https://shopify.dev/api/usage/rate-limits.

Optimizing API usage

Query efficiency

When querying for data within a task, use GraphQL whenever possible, rather than using Liquid objects. GraphQL is much more resource-efficient, and usually results in greater operational throughput.

When working with large volumes of data, use a bulk operation. This way, Shopify bears the burden of collecting all relevant data, without in any way playing against the Shopify API rate limit for Mechanic.

Task configuration

Keep an eye on tasks that are running at the same time, competing for resources. The Shopify API rate limit is shared across each store's entire Mechanic account, which means that simultaneously-running tasks may be in competition for Shopify API usage. Adjustments to task timing (possibly using subscription offsets) can be useful, when making sure that tasks aren't competing. And, in some cases, it may be useful to decrease the overall concurrency limit for a Mechanic account, by emailing [email protected].

Shopify Plus

In high-volume scenarios for Shopify Plus accounts, Mechanic's performance can be improved by creating a custom Shopify app, having the same permissions that you've granted to Mechanic. Because this private app represents your explicit control and intent, it usually comes with a higher API rate limit. (And, in some cases, Shopify can grant this custom app a higher API usage limit, upon request.) By providing Mechanic with this custom app's Shopify Admin API access token, you can extend this higher limit to Mechanic.

This feature is also useful for accessing Plus-only APIs, which are only available to custom Shopify apps. Notably, this includes gift cards (using the gift card object).

This setting can be found in the Mechanic account settings, in the Permissions area. (This setting is only shown for Shopify Plus accounts.) Before adding your API token, you must ensure that the private app has every access scope that Mechanic requires. A list of current required access scopes is provided just below the token field.

Once configured, this custom API token will be used for all user-configured Shopify operations, wherever supported. (It will not be used when querying for publications, since this resource is only accessible to public apps like Mechanic.)

Creating products in bulk

Auto-tag orders by originating staff member

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

Defining preview events

During , Mechanic scans the task's . For each 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 .)

  • Defining preview event data is usually simpler than defining .

    • Stubbing the event variable (or any of the ) removes any intelligence from the objects Mechanic generates from event data, a drawback avoided by defining a preview event and its data. Using the 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.

Preview event definitions cannot provide for return values from Shopify query operations (i.e. output from the , or the result of traversing Shopify Liquid objects, as in customer.orders.first). For those purposes, use the technique.

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...

... 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.

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.

Environment variables

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

Environment variables may be reassigned as needed. (When preparing a task preview, this may be a necessary technique.) To learn more, see .

The 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:

Remember, Mechanic does not permit access to the Shopify API during . Using this unless statement ensures that reloading only happens during a live event.

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 .

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 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 does not contain a "name" property). On the other hand, Mechanic supports customer.created_at, while Shopify themes do not.

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 ).

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 ).

Examples

Actions

In Mechanic, an action is an instruction for performing work that has an effect. Actions are generated by , in response to . Each action has a type, specifying the class of operation to be performed, and options, providing specifics about what that operation will do.

Actions are defined by tasks using , which are simple JSON objects specifying an action's type and options. Action objects can be constructed using the .

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:

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.

Integrations

Mechanic maintains a set of integration actions, offering first-class support for several external services.

Action
Integrated service
Purpose

Cache

The Cache action allows developers to interact with the store's Mechanic , 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).

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.

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.

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.

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 .

When a command is given using , 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.

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.

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

Set a value, explicitly expiring in 1 minute

Clear a value

Action objects

An action object defines work to be performed by an , after the task is fully finished rendering. Action objects are most easily generated using the .

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

Use the 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.

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:

Defining an action

Type

The action type is always a string, having a value that corresponds to (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.

Meta

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

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

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

Runs

, , and 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 .

    • If the originating task , each action run 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.

Most runs are scheduled to be performed immediately. Some runs may be for the future. Some runs may be , 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 , or by a , by the , or by an .

  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 , 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 , and renders it using the associated event. The result of the task run is the set of JSON 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 for either the GraphQL or REST Admin API has been reached, Mechanic will skip over task runs and over 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

Requesting

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

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

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, to create something that suits your needs specifically.

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 .

Where can I file a task request?

Head to . :)

Auto-tag customers by sales channel

Send recurring reminders about unpaid orders

Variable

Contents

shop

An object containing Shopify's REST representation of the current Shopify store

event

An object containing information about the current event

cache

The current store's Mechanic cache object, supporting lookups for cached values

task

An object containing information about the current task

options

An object containing task options, configured by the user

{% unless event.preview %}
  {% assign customer = customer.reload %}
{% endunless %}
Stub data
Shopify variables
event preview
subscriptions
Shopify's REST Admin API reference
Shopify's REST representation of the customer resource

Action

Purpose

Cache

Performing operations on the store's Mechanic cache

Echo

Debugging; displays the options that it is provided, with no side-effects

Email

Sending transactional email

Event

Generating custom user events

Files

Generating files of various types, storing them at a temporary Mechanic-provided URL

FTP

Performing FTP file uploads and downloads

HTTP

Performing HTTP requests

Google Drive

Google Drive

Upload files to Google Drive

Google Sheets

Google Sheets

Create, Update, Export Google Sheets

Flow

Shopify Flow

Sending customer, order, product, and general triggers to Shopify Flow

Shopify

Shopify Admin API

Sending requests to the Shopify Admin API, supporting both REST and GraphQL

Report Toaster

Report Toaster

Requesting reports from Report Toaster, or updating data within Report Toaster

tasks
events
action objects
action tag
Responding to action results
{
  "action": {
    "type": ACTION_TYPE,
    "options": ACTION_OPTIONS,
    "meta": ACTION_META
  }
}
{
  "action": {
    "type": "shopify",
    "options": [
      "post",
      "/admin/customers/1234567890/send_invite.json",
      {}
    ],
    "meta": {
      "invite_reason": "alpha",
      "customer_email": "[email protected]"
    }
  }
}
{% 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 %}
action
action tag
action tag
Responding to action results
a supported action
Responding to action results

Unscheduled

The run has not been assigned a time to be performed

Scheduled

Scheduled to be performed, but that time has not yet arrived

Due

The run is ready to be performed, and is waiting for a runner

Started

The run is being performed

Failed

The run has been performed, and an error has been recorded

Succeeded

The run has been performed, without errors

Events
tasks
actions
action type
subscribes to mechanic/actions/perform
scheduled
retried
Shopify webhook
user webhook
Mechanic scheduler
Event action
offset
Liquid code
action objects
Shopify action
{% 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 %}
task preview
subscriptions
event topic
Previews
stub data
subject variables
Order object
shopify filter
stub data
Shopify API version

Advanced settings

{% action "files" %}
  {
    "image_from_url.png": {
      "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
    }
  }
{% endaction %}
{% action "files" %}
  {
    "plain.txt": "This\nis\na\nmulti-line\nplaintext\nfile."
  }
{% endaction %}
{% 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 %}
{% 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 %}
{% action "echo", foo: "bar", baz: "qux" %}
{
  "action": {
    "type": "echo",
    "options": {
      "foo": "bar",
      "baz": "qux"
    }
  }
}
{% action "echo", "foo", "bar", "baz" %}
{
  "action": {
    "type": "echo",
    "options": [
      "foo",
      "bar",
      "baz"
    ]
  }
}
{% action "echo", "foo" %}
{
  "action": {
    "type": "echo",
    "options": "foo"
  }
}
{% action "echo" %}
  {
    "foo": "bar",
    "baz": "qux"
  }
{% endaction %}
{
  "action": {
    "type": "echo",
    "options": {
      "foo": "bar",
      "baz": "qux"
    }
  }
}
task code
error object
{
  "action": {
    "type": "echo",
    "options": {
      "__error": "Forcing an error!"
    }
  }
}
{% action "echo", __error: "Forcing an error!" %}
{% action "cache", "incr", "foo" %}
{
  "action": {
    "type": "cache",
    "options": [
      "incr",
      "foo"
    ]
  }
}
{% action "cache", "setex", "foo", 5, "bar" %}
{% action "cache" %}
  {
    "set": {
      "key": "foo",
      "value": 5
    }
  }
{% endaction %}
{% action "cache", "set", "foo", 5 %}
{% action "cache" %}
  {
    "setex": {
      "key": "foo",
      "ttl": 60,
      "value": 5
    }
  }
{% endaction %}
{% action "cache", "setex", 60, "foo" %}
{% action "cache" %}
  {
    "del": {
      "key": "foo"
    }
  }
{% endaction %}
{% action "cache", "del", "foo" %}
cache
positional options
verbose options
{
  "action": {
    "type": "cache",
    "options": {
      "incr": {
        "key": "foo",
        "ttl": 600
      }
    }
  }
}
{% action "cache" %}
  {
    "incr": {
      "key": "foo",
      "ttl": 600
    }
  }
{% endaction %}
work with a developer one-on-one
[email protected]
https://mechanic.canny.io/task-requests

Files

The Files action evaluates its options using 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, 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. In this way, many files may be defined and generated by a single Files action.

Result

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

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 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.

mechanic/user/trigger
mechanic/actions/perform
{% 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": "[email protected]",
      "subject": {{ action.run.result | size | append: " file(s) generated" | json }},
      "body": {{ email_body | json }}
    }
  {% endaction %}
{% endif %}

File generators

File generators are invoked by 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 can make choices about what files to generate, and what to do with the results.

File generator

Purpose

Decodes base64-encoded content, returning a file containing the results

Renders HTML using a full Webkit browser, returning a PDF file of the results

Allows defining file contents using a plain string, instead of a file generator object

Downloads and returns a file

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.

"But why?", you may well ask!

Mechanic allows action run results to be fed back into the system (via mechanic/actions/perform). 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. 🤷

Object structure

File generator objects, like 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.

The plaintext file generator is invoked implicitly by supplying a string, instead of supplying the usual file generator object.

In the following example, a 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.

{% 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

Uses file generators to prepare email attachments

Uses file generators to prepare temporary URLs, from which the generated files can be downloaded

Uses file generators to prepare FTP uploads

Adds generated files to a multipart/form-data HTTP request

Shopify API version

Each task is configured with a specific Shopify API version, 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

  • All Shopify API calls performed by the Shopify action, including bulk operations

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.

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.

Changing versions

The selector for a task's Shopify API version is available in Advanced mode, below the task code area.

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 will no longer work as of the Feb 1, 2025 REST deprecation date.

REST - product resource lookup from line item
{%- 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 -%}

The code above could be utilized directly in a multiline task option field. 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.

GraphQL - order query with line item products
{%- 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 -%}

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.

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.

{% assign email_body = options.email_body__multiline_code_required | strip | newline_to_br %}
Email body task option
Email body task option using code flag

The embedded GraphQL query will work without or without using the "code" flag.

Perform action runs in sequence

Mechanic's run system 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.

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.

Maintain a tag for orders processed today

GraphQL in Liquid

Tasks may use the to convert GraphQL query strings into simple result objects, by sending the query to the . The easiest way to build these queries is via the , which allows queries to be interactively constructed.

The shopify filter does not support running mutations (i.e. writing Shopify data via GraphQL). To run mutations, use the .

Usage

The accepts a GraphQL query string, and returns everything back from Shopify's GraphQL admin API. This means that reading back GraphQL data is as easy as this:

The also supports GraphQL variables!

If you're working with multiple pages of data, you might use set up a forloop, using a cursor to retrieve page after page:

You'll note that this code includes stub data when running during a preview event. This technique is extremely useful for generating , by allowing you to exercise your entire task script.

The hardest part of using GraphQL in Mechanic is writing the query itself. :) For help with this, we recommend installing . It provides an environment where, using auto-complete and built-in documentation, you can rapidly build the right query for your task.

Note: GraphQL queries (excluding whitespace) are limited to 50,000 characters. That's a hard limit, enforced on Shopify's end – if you bump up against it, you'll need to adjust your query strategy to always stay under that limit. If you're saving large values to a metafield, for example, consider separating those values using GraphQL variables, keeping the query itself trim. Learn more about this scenario using the , or with the .

Use GraphQL when...

  • ... you want to make things more efficient. GraphQL is fantastic for being really precise about what data you want, which makes your tasks run in less time: no more looping through collections to find your data, and no more downloading data you don't require.

Don't use GraphQL when...

  • ... it's easier and more readable to use Liquid objects, unless performance becomes an issue. Ultimately, the most important thing is that your task works well tomorrow – and that includes making sure that whoever works on it next understands what you're doing. If that means using a quick-and-simple Liquid lookup over a moderately-more-complex GraphQL lookup, go for it.

  • ... you find yourself staring at nested loops. Looping through all orders is one thing – it's quite another to loop through pages of orders and loop through pages of line items within each order. For those scenarios, whenever possible, use a bulk operation.

Liquid objects

Mechanic-flavored Liquid comes with a complement of , each of which is tied to a resource in the . Many objects support access to related objects via lookups (e.g. {{ shop.customers[customer_id].orders.first }}); in this way, the REST API can be traversed by resource.

Shopify is deprecating some of the Shopify Admin REST API. The first round of deprecations involve the product and variant endpoints. Read the deprecation notice .

Our recommendation is to use going forward. The and objects will cease to work on on Feb 1, 2025 due to the changes being made by Shopify. It appears that Shopify will gradually phase out the REST API over time.

All of our will be ported to use GraphQL only, which will provide a model for how you can update your custom tasks. You'll be able to update your non-customized library tasks with a click of a button ☺️

Access to these Liquid objects varies, based on the context in which Liquid is rendered. For example, a task that subscribes to shopify/customers/create will have access to the object in its code, via a variable called customer. To learn more about how these objects are made available to task code, see .

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 does not contain a "name" property). On the other hand, Mechanic supports {{ customer.created_at }}, while Shopify themes do not.

Usage

Each task is given a set of to work with, out of the box. Mechanic's task code editor will tell you which ones are available. For example, for a task responding to a shopify/orders/ event, you might see this:

The , , , and objects are always available for tasks; the object (as in this example) contains the order to which the current event relates.

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 will interrupt the preview, and visibly communicate the error to the user. This makes error objects a useful way to validate .

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.

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

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 .

Writing data

There is a single correct answer for writing data to Shopify: the action. :)

MIME type
Base64
PDF
Plaintext
URL
ZIP
Email
Files
FTP
HTTP
{% capture query %}
  query {
    shop {
      name
    }
  }
{% endcapture %}

{% assign result = query | shopify %}

{% log result.data.shop.name %}
{% assign cursor = nil %}
{% assign total_inventory = 0 %}

{% for n in (0..100) %}
  {% capture query %}
    query {
      products(
        first: 250
        after: {{ cursor | json }}
      ) {
        pageInfo {
          hasNextPage
        }
        edges {
          cursor
          node {
            totalInventory
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "products": {
            "edges": [
              {
                "node": {
                  "totalInventory": -4
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

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

  {% for product_edge in result.data.products.edges %}
    {% assign product = product_edge.node %}
    {% assign total_inventory = total_inventory | plus: product.totalInventory %}
  {% endfor %}

  {% if result.data.products.pageInfo.hasNextPage %}
    {% assign cursor = result.data.products.edges.last.cursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}
shopify Liquid filter
Shopify GraphQL Admin API
Shopify Admin API GraphiQL explorer
Shopify action
shopify filter
shopify filter
dynamic preview actions
Shopify's GraphiQL app
Shopify action
shopify Liquid filter
{
  "error": ERROR_DETAILS
}
during preview
task options
error tag
Shopify

Bulk operations

Mechanic supports Shopify's bulk operations GraphQL API, allows developers to submit a query to Shopify for asynchronous processing, and making the results available to the task once complete.

This approach dodges the issues inherent in synchronous methods of reading data (like GraphQL via the shopify filter, or REST via Liquid objects). Unlike these methods, the bulk operations API does not exhaust Shopify API limit for your Mechanic account, and therefore does not slow down other tasks. It also does not require any special logic for pagination, since Shopify handles all data collection.

This article reviews Mechanic's support for bulk operations. If this feature is new to you, start by reading Shopify's tutorial on the bulk operations API: https://shopify.dev/tutorials/perform-bulk-operations-with-admin-api

Shopify only permits apps to run a single bulk operation at a time, per store. Actions that create another bulk operation, while one is already running, will return an error.

This area deserves improvement! To discuss the future of this behavior, visit the related feature request: https://mechanic.canny.io/futures/p/bulk-operation-retries

Usage

Creating a bulk operation

Use the Shopify action to execute a bulkOperationRunQuery mutation (see Shopify's tutorial). Mechanic will detect this mutation, and will begin monitoring the bulk operation in progress.

Subscribing to the results

Add a subscription to mechanic/shopify/bulk_operation.

Once Mechanic detects that the bulk operation has been completed, the platform will automatically re-invoked the same task with an event having the topic "mechanic/shopify/bulk_operation", containing the bulk operation's data, when the bulk operation is complete. (As with mechanic/actions/perform, Mechanic will only invoke the current task when the bulk operation completes; no other task will be notified.)

Accessing the results

When processing a mechanic/shopify/bulk_operation event, the task will have access to an environment variable called bulkOperation, containing all attributes of the bulk operation (docs).

The set of objects returned by the bulk operation is made available as bulkOperation.objects, allowing you to scan returned data immediately, using an expression like {% for object in bulkOperation.objects %}.

In most cases, every object that has an ID will appear as a separate object, in the same set of objects. For example, if a product and five variants are returned, there will be six objects returned – the variants are not nested inside of the product object.

The JSON objects returned from bulk operation queries each include a "__parentId" attribute for connected objects, containing the parent object's ID. To make managing task scripts easier, Mechanic allows you to simply call {{ object.__parent }} to look up an object's parent.

Because all objects are returned as peers in a flat set, we've found that processing objects is easiest when you can easily identify each object by its type. To that end, try including __typename in the list of selections for each node, right alongside id.

This technique allows the array of objects to be quickly filtered by type:

{% assign customers = bulkOperation.objects | where: "__typename", "Customer" %}

Example

mechanic/user/trigger
mechanic/shopify/bulk_operation
{% if event.topic == "mechanic/user/trigger" %}
  {% capture bulk_operation_query %}
    query {
      customers {
        edges {
          node {
            __typename
            id
            email
          }
        }
      }
    }
  {% endcapture %}

  {% action "shopify" %}
    mutation {
      bulkOperationRunQuery(
        query: {{ bulk_operation_query | json }}
      ) {
        bulkOperation {
          id
          status
        }
        userErrors {
          field
          message
        }
      }
    }
  {% endaction %}
{% elsif event.topic == "mechanic/shopify/bulk_operation" %}
  {% assign customers = bulkOperation.objects | where: "__typename", "Customer" %}
  {% assign emails = customers | map: "email" %}
  {% log emails: emails %}
{% endif %}

More examples

  • Task: Calculate total quantities purchased by SKU

  • Task: Sync order timeline comments to the customer note

  • Task: Auto-tag orders with their tracking numbers

Use bulk operations when...

  • ... your task needs to collect and process a lot of data. Tasks responding to bulk operations operate with a higher memory allowance than other tasks, decreasing the chances of your task being terminated for memory exhaustion.

  • ... paginating for data would be too complicated. Pagination in GraphQL can be tricky when using nested resources.

Don't use bulk operations when...

  • ... you only need a little bit of data. Use the shopify filter instead.

  • ... you're responding to a Shopify event, and the data you need comes along with the event data. Use Liquid objects instead.

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.

Working on getting better at task-writing? See Writing a high-quality task.

Assignments

Liquid objects
Shopify Admin REST API
here
GraphQL
product
variant
library tasks
Customer
environment variables
Shopify's REST representation of the customer resource
environment variables
cache
event
options
shop
order

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, including text files, PDFs, CSVs, and HTML files. Mechanic interacts with Google Drive via the Google Drive API, using OAuth2 for authentication.

Options

Option
Type
Description

account

string

Required: the Google account email address to authenticate with

uploads

hash

Required: a has specifying files to upload and their contents

Uploads hash structure

The uploads hash supports these properties:

Property
Type
Description

overwrite

boolean

Optional: when true, files with matching names will be overwritten. Defaults to false

[path/filename]

string | hash

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 .

Authentication

This action requires connecting a Google account with the appropriate Drive permissions. To connect an account:

  1. Go to the Settings screen

  2. Click Authentication

  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

{% action "google_drive" %}
  {
    "account": "[email protected]",
    "uploads": {
      "simple.txt": "Hello world!"
    }
  }
{% endaction %}

Multiple Files with Overwrite

{% action "google_drive" %}
  {
    "account": "[email protected]",
    "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

{% action "google_drive" %}
  {
    "account": "[email protected]",
    "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

{% 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:

{
  "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

{
  "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"
    }
  }
}

PDF

The PDF file generator accepts an object containing an HTML string, and uses Pdfcrowd to render it as a PDF document. Pdfcrowd employs the 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

Option

Description

html

Required; a string containing the HTML, CSS and JavaScript to be rendered

...

Additional Pdfcrowd API options supported; see below

{
  "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.

Example

{% 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 %}
{
  "action": {
    "type": "files",
    "options": {
      "file.pdf": {
        "pdf": {
          "html": "\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Liu+Jian+Mao+Cao&display=swap\" rel=\"stylesheet\">\n<style>p { font-family: 'Liu Jian Mao Cao', cursive; }</style>\n\n<p>Almost before we knew it, we had left the ground.</p>\n<div id=\"tester\" style=\"width:100%;height:40vh;\"></div>\n\n<script src=\"https://cdn.plot.ly/plotly-2.2.0.min.js\"></script>\n<script>\n  // from https://plotly.com/javascript/getting-started/\n  TESTER = document.getElementById('tester');\n\tPlotly.newPlot( TESTER, [{\n\tx: [1, 2, 3, 4, 5],\n\ty: [1, 2, 4, 8, 16] }], {\n\tmargin: { t: 0 } } );\n</script>\n",
          "page_width": "7in",
          "page_height": "5in",
          "margin_top": "10mm",
          "margin_right": "10mm",
          "margin_bottom": "10mm",
          "margin_left": "10mm"
        }
      }
    }
  }
}

Flow

The Flow action sends data to Shopify Flow, arriving as one of four possible Flow triggers.

This page is about the Mechanic action that sends data to Shopify Flow. For a review of Mechanic's entire integration with Flow, see .

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.

These resource options only accept fully-numeric resource IDs (i.e. 12345). They do not accept global IDs (i.e. gid://shopify/Customer/12345).

Resource option
Flow trigger

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.

Option
Type
Default

Usage

For a detailed review of usage, see .

Converting tasks from Shopify REST to GraphQL

Important Notice

Shopify is deprecating the Shopify Admin REST API which the Mechanic REST objects depend on. The first round of deprecations involve the product and variant endpoints. Read about the deprecation and .

Use the going forward. The and objects will cease to work on on Feb 1, 2025 due to the changes being made by Shopify. Shopify will phase out the REST API completely over time, you can read more about this .

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 objects, queries, and mutations.

  2. Review how to use GraphQL in Mechanic Start and peruse the 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 *️

    • product.metafields

    • product.variants *️

    • 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 .

  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.

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 , 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.

Reading data

Mechanic supports several methods of reading data. The following articles discuss usage, and when each technique might (or might not) be appropriate:

Email the customer when tracking numbers are added to their order

file generator object

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"

user_boolean

Boolean

false

user_email

Email address

"[email protected]"

user_number

Number

0

user_string

String

""

user_url

URL

"https://mechanic.invalid/"

Shopify Flow
Shopify Flow
here
here
GraphQL
product
variant
here
Shopify GraphQL Admin API
here
task library
Shopify's GraphiQL Explorer
support for 2 thousand variants per product
Liquid objects
GraphQL in Liquid
Bulk operations
The Shopify action
Demonstration: Auto-tag new orders, with scheduled reconciliation – Mechanic, ecommerce automation platform for Shopify

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. 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.

In Basic mode, documentation is displayed below the task options.

Markdown

Task documentation may be formatted with Markdown, a common syntax for text formatting.

The following table comes from markdownguide.org/cheat-sheet, and is presented with no changes, under the CC BY-SA 4.0 license.

Element

Markdown Syntax

# H1 ## H2 ### H3

**bold text**

_italicized text_

> blockquote

- First item - Second item - Third item

1. First item 2. Second item 3. Third item

code

---

[title](https://www.example.com)

![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.

🎬 Watch: Keyboard Shortcuts Demo

Action

Shortcut

Description

Toggle Bold

: ⌘+B : Ctrl+B

Adds or removes Bold formatting on selected text or current word.

Toggle Italic

: ⌘+I : Ctrl+I

Adds or removes Italic formatting on selected text or current word.

Toggle Strikethrough

: ⌘+Shift+X : Ctrl+Shift+X

Adds or removes Strikethrough on selected text or current word.

Toggle Heading

: ⌘+Shift+H : Ctrl+Shift+H

Cycles through Heading formats.

Toggle Inline Code

: ⌘+Shift+E : Ctrl+Shift+E

Adds or removes Inline Code formatting on selected text or current word.

Toggle Code Block

: ⌘+Shift+E : Ctrl+Shift+E

Adds or removes a Code Block on multiline selection or blank line.

Toggle List

: ⌘+Shift+L : Ctrl+Shift+L

Cycles through List formats.

Toggle Link

: ⌘+Shift+K : Ctrl+Shift+K

Adds or removes Link formatting.

Toggle Blockquote

: ⌘+Shift+B : Ctrl+Shift+B

Adds or removes Blockquote formatting.

Toggle Image

: ⌘+Shift+I : Ctrl+Shift+I

Adds or removes Image formatting.

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)

Stub data

Stub data is hard-coded into a task, providing an unchanging source of data for . It is an important tool when generating . Stub data may be used for user-defined variables, but may also override as needed.

For controlling preview event data (i.e. the values in event.data, and values found in ), use to cleanly specify these values outside of the task code.

Stubbing Liquid variables

Most tasks make decisions based on the 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 .

In simple cases, replacement objects may be constructed using the tag.

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!

It's also possible to construct this data using .

Stubbing GraphQL data

Mechanic makes GraphQL data available to tasks via the 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 filter. Sample JSON is easy to generate using .

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.


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.

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

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.


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.


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.

To assist with generating an object query block, you can use the 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).

To see a code diff from a Mechanic library task that was recently converted in this manner, click , and review the code variations between the {% if event.topic == "shopify/orders/create" %} blocks.

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.

Example

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:

→

{% 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 %}
{% 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 %}
{% 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 %}
{% 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": "[email protected]"
                }
              }
            ]
          }
        }
      }
    {% 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 %}
previews
dynamic preview actions
environment variables
event subject variables
defined preview events
Liquid variables
event subject variables
assign
parse_json
shopify
parse_json
Shopify's GraphiQL app
REST - simple product lookup
{% assign product = shop.products[product_id] %}
GraphQL - simple product query
{% capture query %}
  query {
    product(id: {{ product_id | json }}) {
      id
      # additional fields as needed
    }
  }  
{% endcapture %}

{% assign result = query | shopify %}

{% assign product = result.data.product %}
REST - example product logging
{% 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
%}
GraphQL - example product logging
{% 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
%}
REST - Basic product tagging task
{% 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 %}
GraphQL - Basic product tagging task
{% 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 %}j
"object_query" snippet
here
Platform / Integrations / Report Toaster / Action
**text**
_text_
~text~
→ H1     → H2      → H3       → Plain Text
→ # text → ## text → ### text → text 
`code`
```
code
```
→ Unordered List → Ordered List → Plain Text
→ - text         → 1. text      → text
[text](url)
> text 
![](url)
Heading
Bold
Italic
Blockquote
Unordered List
Ordered List
Code
Horizontal Rule
Link
Image
{
  "base64": BASE64_ENCODED_VALUE
}
{% 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 %}
{
  "action": {
    "type": "files",
    "options": {
      "image_from_base64.jpg": {
        "base64": "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"
      }
    }
  }
}

Event

The Event action is for generating custom events in the User event domain. 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 of the event responsible for the current action.

Options

Option

Description

topic

Required; a string specifying an 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 may be used, and will be respected.

Examples

Using the Event tag

{% assign data = hash %}
{% assign data["foo"] = "bar" %}

{% action "event", topic: "user/foo/bar", data: data %}
{
  "action": {
    "type": "event",
    "options": {
      "topic": "user/foo/bar",
      "data": {
        "foo": "bar"
      }
    }
  }
}

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.

{% 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 %}
{
  "action": {
    "type": "event",
    "options": {
      "topic": "user/foo/bar",
      "data": {
        "foo": "bar"
      },
      "task_id": "293b7040-6689-4eb1-8b5d-64f4d33eb2ae"
    }
  }
}

Notes

You can limit a task to itself by referencing it's own task.id

See 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.

{% 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 %}
{
  "action": {
    "type": "event",
    "options": {
      "topic": "user/foo/bar",
      "task_id": "293b7040-6689-4eb1-8b5d-64f4d33eb2ae",
      "run_at": 1613158259,
      "data": {
        "foo": "bar"
      }
    }
  }
}

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.

shopify/orders/create
user/orders/unpaid_reminder
{% 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.

shopify/orders/create
user/orders/unpaid_reminder+1.day
{% 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 %}

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:

{
  "log": LOG_DETAILS
}

The log details can be any JSON value.

Log objects are most easily generated using the log tag.

Log objects appear wherever task run results are visible, including the task preview and when viewing an event.

A log object visible in a task preview
A log object visible in a task run's result

Previews

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.

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 (to construct relevant scenarios at the event level) or use (to swap in predefined values for , or for the results of data that would otherwise come from the Mechanic cache or the Shopify Admin API).

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 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 action containing a 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 filter receiving a customer-related GraphQL query, it will prompt for the read_customers scope.

Some GraphQL mutations have multiple potential scope requirements, like or . 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.

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 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 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.

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.

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 , 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.

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.

There are two techniques available for "steering" the task towards desired outcomes during preview.

  1. Use 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 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.

event topic
{% 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 %}
{% 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 %}
shopify/orders/create
Example
{% if some_evaluated_condition %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ order.admin_graphql_api_id | json }}, tags: "web") {
        userErrors { field, message }
      }
    }
  {% endaction %}
{% endif %}
define preview events
stub data
environment variables
Email
Shopify
customerCreate
shopify
tagsAdd
metafieldsSet
defines its own preview event
the User domain
dynamic preview actions
defined preview events
stub data

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)

  • A basic knowledge of Liquid (need a refresher?)

  • A basic knowledge of JavaScript (need a refresher?)

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

  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 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

  • Mechanic webhooks

  • The csv Liquid filter

  • The Email action

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

Start with the 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 feature to have our code automatically loaded into the online storefront. (Under the hood, Mechanic leverages Shopify's ScriptTag API.)

For this tutorial, I created a development store and installed the Debut theme. 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.

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.

Next, we're going to write some JavaScript that listens for thesubmit 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.

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!

// 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();
      });
    },
  );
}

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.

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.

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

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.)

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 }}.

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.

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 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 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.

{% 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 %}

When writing a task, it's important to think about 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.

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

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.

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. :)

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:

{"name":"Receive contact form for CRM","options":{"recipient_email_address__email_required":"[email protected]","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"}

Shopify admin action links

Use these access points within the Shopify admin to send resources like orders and customers to Mechanic for processing.

For supported resources, you'll find "Send to Mechanic" links within the Shopify admin. These links point to a Mechanic app URL that translates selected Shopify resources into Mechanic events for on-demand processing. Depending on the user's resource and task selections, their submission may result in one or more new events. Event data consists of the latest stable REST Admin API representation of the selected resource(s).

Important: Starting Feb 1, 2025, the product and variant resource types will no longer include full REST Admin API data in these events. Instead, only resource IDs (id and admin_graphql_api_id) will be included, due to Shopify REST API deprecations. If you need additional product or variant details (e.g. title, vendor, etc.), you must fetch them via Shopify's GraphQL API.

Tasks can qualify for this style of on-demand Shopify resource processing by subscribing to event topics like mechanic/user/{resource} (singular) for individual processing, and mechanic/user/{resources} (plural) for batch processing. See Supported resources below for a complete table of resources and event topics.

Shopify admin action links are a form of run link, and as such equivalent URLs may be constructed manually using known resource IDs. For example, an admin order notification email could be written to include such a link, allowing the email recipient to send the relevant order to Mechanic.

Supported resources

Resource object
URL parameter
Individual mode event topic
Batch mode event topic

customer

mechanic/user/customer

mechanic/user/customers

order

mechanic/user/order

mechanic/user/orders

draft_order

mechanic/user/draft_order

mechanic/user/draft_orders

product

mechanic/user/product

mechanic/user/products

variant

mechanic/user/variant

mechanic/user/variants

collection

mechanic/user/collection

Note: After Feb 1, 2025, product and variant events will only include resource IDs:

{
 "id": 123456,
 "admin_graphql_api_id": "gid://shopify/Product/123456"
}

For more data, use Shopify's GraphQL API.

Mechanic distinguishes between "individual" and "batch" modes to enable a greater variety of possible workflows. Some tasks may benefit from having knowledge of all selected resources at the same time within the same task run, while some tasks may be more suited to receiving a single resource at a time.

For example, tasks that are created to print deliberately-sorted pick lists might benefit from batch mode, whereas tasks aimed at refunding specific orders might benefit from individual mode.

Individual mode

This mode creates an event for each selected Shopify resource, resulting in as many events as there were selected resources. Tasks subscribing to an individual mode event topic (e.g. mechanic/user/order or mechanic/user/customer) may use an environment variable named after the singularized resource name, e.g. order or customer.

{% log product_title: product.title %}

Batch mode

This mode creates one (1) event whose data consists of an array holding all selected Shopify resources, up to a maximum of 50 resources total. No matter how many resources were selected, only one event is created. Tasks subscribing to a batch mode event topic (e.g. mechanic/user/orders or mechanic/user/customers) may use an environment variable named after the pluralized resource name, e.g. orders or customers.

{% for product in products %}
  {% log product_title: product.title %} 
{% endfor %}

Link usage

1. Selecting resource(s)

For supported Shopify resources, the "Send to Mechanic" action link can be found in the "More actions" menu. Click this link to route the currently-viewed Shopify resource to Mechanic.

A Shopify admin action link on the details page for a specific order

For supported Shopify resources, the "Send to Mechanic" link is also available as a bulk action, supporting selections of up to 50 resources at once. Find the "Send to Mechanic" link after in the menu of additional actions, in the floating action list that appears after selecting one or more resources.

A Shopify admin action link on a list of orders

Sending one (1) resource to Mechanic via bulk action (on a resource list page) is the same as sending an individual resource to Mechanic via the "More actions" menu (on that resource's detail page). The two paths arrive in the same place, and the choice between batch mode and individual mode is available either way.

Shopify admin action links are a form of run link, following this pattern:

https://admin.shopify.com/store/$SHOP_NAME/apps/mechanic/tasks/run?resource_type=$RESOURCE_TYPE&ids[]=123&ids[]=456

These URLs may be manually constructed, using resource type parameter values drawn from the Supported resources table above, and using integer REST resource IDs.

2. Configuring task run(s)

Using a Shopify admin action link brings the user to the page shown below, in which selected resources are shown for preview, and a choice of processing mode is offered.

Select to send your resources as an array of resources in a single event, or as a single resource in many events

After selecting a processing mode, select from the available compatible tasks – i.e. from enabled tasks that subscribe to a relevant event topic. You can send this event to one or more tasks that subscribe to the correct event topic.

Task usage

To qualify a task to receive these events, subscribe to an event topic from the Supported resources table above.

For these events, Mechanic makes available an environment variable named after the third term in the event topic. For example, a mechanic/user/order event will make available a variable called order, which contains an Order object with data pulled from the Shopify Admin REST API.

Note: For product and variant, as of Feb 1, 2025, only an id and admin_graphql_api_id are provided. Additional fields (e.g. product.title) must be fetched from Shopify GraphQL.

By contrast, a mechanic/user/orders event will make available a variable called orders, which contains an array of Order objects.

Event data for these topics is often very similar to data from Shopify events, but there are occasionally differences. For example, shopify/orders/* events do not include customer data. By contrast, customer data is included in the Shopify Admin REST API representation for the Order resource. Therefore, event data for mechanic/user/order and mechanic/user/orders events do contain information about the customer, unlike shopify/orders events.

Your task subscriptions will make them eligible to recieve these events from the Shopify admin

Retrofitting existing tasks

Tasks that already subscribe to related Shopify event topics lend themselves well to invocation via Shopify admin action links. For example, a task subscribing to shopify/orders/create may be adapted to these action links by adding mechanic/user/order to the task's subscriptions.

Additional changes may be necessary. Always test thoroughly, and follow best practices.

Example

The Send a PDF invoice when an order is created task, from the Mechanic task library, is well-suited to this kind of adaptation. To illustrate, here is the portion of the task code that addresses the order itself:

{% if order.email != blank and order.financial_status == "pending" %}
  {% action "email" %}
    {
      "to": {{ order.email | json }},
      "subject": {{ options.email_subject__required | strip | json }},
      "body": {{ options.email_body__multiline_required | strip | newline_to_br | json }},
      "reply_to": {{ shop.customer_email | json }},
      "from_display_name": {{ shop.name | json }},
      "attachments": {
        {{ options.pdf_attachment_filename__required | json }}: {
          "pdf": {
            "html": {{ options.pdf_html_template__code_multiline_required | json }}
          }
        }
      }
    }
  {% endaction %}
{% endif %}

Because this task straightforwardly references the order environment variable, this task is a good candidate for a subscription to mechanic/user/order. Once modified in that way, the task will be able to send PDF invoices on demand, whenever a user sends orders to the task via a "Send to Mechanic" link.

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.

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.

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.

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

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.

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

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.

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.

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 - 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).

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.

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

Next steps

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

If you have any questions, head to our community Slack.

Customer
Order
Draft order
Product
Product variant
Collection

Google Sheets

The Google Sheets action allows you to interact with Google Sheets. It supports creating new spreadsheets, appending data to existing sheets, and exporting spreadsheets in various formats. Mechanic interacts with Google Sheets via the Google Sheets API, using OAuth2 for authentication.

Due to Google security restrictions, Mechanic can only access spreadsheets that were created through Mechanic itself. To work with Google Sheets:

  • First create a spreadsheet using the "create_spreadsheet" operation

  • Store the returned spreadsheet ID for later use

  • Then use operations like "append_rows" on this spreadsheet

See this great example in the task library.

Options

Option
Type
Description

account

string

Required: the Google account email address to authenticate with

operation

string

Required: the operation to perform. One of: "append_rows", "create_spreadsheet", "export_spreadsheet"

spreadsheet_id

string

Required: for append_rows and export_spreadsheet; the ID of the target spreadsheet

title

string

Required: for create_spreadsheet; the title for the new spreadsheet

rows

array

Required: for append_rows and optional for create_spreadsheet; array of arrays containing the data to write

sheet_name

string

Optional: for append_rows; defaults to "Sheet1"

file_type

string

Optional: for export_spreadsheet; the format to export. One of: "xlsx" (default), "csv", "pdf", "html", "ods", "tsv"

folder_path

string

Optional: for create_spreadsheet; the folder path where the spreadsheet should be created (e.g., "reports/2024/monthly")

Operations

append_rows

Adds new rows to an existing spreadsheet.

Required Options

  • account

  • spreadsheet_id

  • rows

Optional Options

  • sheet_name (defaults to "Sheet1")

create_spreadsheet

Creates a new spreadsheet, optionally with initial data.

Required Options

  • account

  • title

Optional Options

  • folder_path (path where spreadsheet should be created)

  • rows (initial data to populate the spreadsheet)

export_spreadsheet

Exports a spreadsheet in various formats.

Required Options

  • account

  • spreadsheet_id

Optional Options

  • file_type

    • xlsx (default)

    • csv

    • pdf

    • html

    • ods

    • tsv

Authentication

This action requires connecting a Google account with the appropriate permissions. To connect an account:

  1. Go to the Settings screen

  2. Click Authentication

  3. Follow the Google account connection flow

File Access

The action can only access spreadsheets it creates, no other spreadsheets in your drive.

Folder Support

When creating spreadsheets, you can specify a folder path to organize your files:

  • Use forward slashes to separate folder names (e.g., "reports/2024/monthly")

  • Folders will be created if they don't exist

  • Can only access folders created by this integration

  • Invalid characters not allowed: < > : " / \ | ? *

Folder Path Examples

reports/monthly           # Two levels deep
data/2024/q1/sales       # Four levels deep
archives/exports/sheets   # Three levels deep

Examples

Append Rows to Existing Google Sheet

{% action "google_sheets" %}
  {
    "account": "[email protected]",
    "operation": "append_rows",
    "spreadsheet_id": "1234567890abcdef",
    "sheet_name": "Orders",
    "rows": [
      ["Order ID", "Customer", "Total"],
      ["1001", "John Doe", "99.99"],
      ["1002", "Jane Smith", "149.99"]
    ]
  }
{% endaction %}

Create New Google Sheet

{% action "google_sheets" %}
  {
    "account": "[email protected]",
    "operation": "create_spreadsheet",
    "title": "Monthly Sales Report",
    "rows": [
      ["Month", "Revenue", "Expenses", "Profit"],
      ["January", "5000", "3000", "2000"],
      ["February", "5500", "3200", "2300"]
    ]
  }
{% endaction %}

Export Google Sheet

{% action "google_sheets" %}
  {
    "account": "[email protected]",
    "operation": "export_spreadsheet",
    "spreadsheet_id": "1234567890abcdef",
    "file_type": "pdf"
  }
{% endaction %}

Dynamic Data Example

{% assign order_rows = array %}
{% assign header_row = array %}
{% assign header_row["Order", "Customer", "Total"] %}
{% assign order_rows[header_row] %}

{% for order in shop.orders %}
  {% assign order_row = array %}
  {% assign order_row[order.name, order.customer.name, order.total_price] %}
  {% assign order_rows[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 %}

Create Google Sheet in a Folder

{% action "google_sheets" %}
  {
    "account": "[email protected]",
    "operation": "create_spreadsheet",
    "folder_path": "reports/2024/monthly",
    "title": "March Sales",
    "rows": [
      ["Date", "Revenue", "Units"],
      ["2024-03-01", "5000", "50"],
      ["2024-03-02", "6000", "60"]
    ]
  }
{% endaction %}

Read Data From Google Sheet

Task subscriptions
mechanic/user/trigger
mechanic/actions/perform
Task code
{% if event.topic == "mechanic/user/trigger" %}
  {% action "google_sheets" %}
    {
      "account": {{ options.google_account__required | json }},
      "operation": "export_spreadsheet",
      "spreadsheet_id":  {{ options.spreadsheet_id__required | json }},
      "file_type": "csv"    
    }
  {% endaction %}
{% endif %}

{% if event.topic == "mechanic/actions/perform" %}
  {% if action.type == "google_sheets" and action.run.ok %}
    {% assign sheet_data = action.run.result.data_base64 | 
    base64_decode | parse_csv: headers: true %}
      {% action "echo" sheet_data %}
  {% endif %}
{% endif %}

Action Responses

The action returns different responses based on the operation performed:

append_rows Response

{
  "spreadsheet_id": string,
  "updated_range": string,
  "updated_rows": number,
  "updated_columns": number,
  "spreadsheet_url": string
}

Example:

{
  "spreadsheet_id": "1234567890abcdef",
  "updated_range": "Sheet1!A1:C3",
  "updated_rows": 3,
  "updated_columns": 3,
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef"
}

create_spreadsheet Response

{
  "spreadsheet_id": string,
  "spreadsheet_url": string,
  "title": string
}

Example:

{
  "spreadsheet_id": "1234567890abcdef",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef",
  "title": "Monthly Sales Report"
}

create_spreadsheet Response with Folder

{
  "spreadsheet_id": string,
  "spreadsheet_url": string,
  "title": string,
  "folder_path": string
}

Example:

{
  "spreadsheet_id": "1234567890abcdef",
  "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1234567890abcdef",
  "title": "March Sales",
  "folder_path": "reports/2024/monthly"
}

export_spreadsheet Response

{
  "spreadsheet_id": string,
  "name": string,
  "size": number,
  "file_type": string,
  "data_base64": string
}

Example:

{
  "spreadsheet_id": "1234567890abcdef",
  "name": "Monthly Sales Report",
  "size": 12345,
  "file_type": "pdf",
  "data_base64": "base64encodeddata..."
}

A screencast illustrating an HTML test, and a path for reaching Pdfcrowd support

FTP

Upload and download files via FTP, FTPS, or SFTP.

The FTP action can upload and download files via FTP, FTPS, or SFTP. The files to be uploaded are evaluated using 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.

A connecting service like Couchdrop 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

Option
Type
Notes

protocol

"ftp", "ftps", or "sftp"

The protocol to use for connection; inferred if omitted

host

String, required

The hostname or IP address of the destination server

port

Number, optional

The server port to connect to

user

String, required

The username for authentication

password

String, optional

The password for authentication

uploads

Hash, optional

An object whose keys are file paths (relative or absolute), and whose values are

downloads

Array, optional

File path strings (relative or absolute) to download

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 (, )

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:

{% 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 %}

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).

{% action "ftp" %}
  {
    ...
    "uploads": {
      "/absolute/path/to/success.txt": "hooray!",
      "relative/path/to/success.txt": "hooray!",
      "../another/relative/path/to/success.txt": "hooray!"
    }
  }
{% endaction %}

Result

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

An FTP action returns the following data structure, most useful in combination with mechanic/actions/perform (see Responding to action results):

{
  "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 filter.

Testing

If a server is unavailable for testing, consider using Couchdrop, with their hosted storage service. This is a (nearly) configuration-free avenue for testing, using my.couchdrop.io for FTP, FTPS, or SFTP.

Alternatively, ngrok 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:

{% 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.

mechanic/scheduler/daily
mechanic/user/trigger
{% 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 %}

Options

accept user configuration via options. Options are created dynamically, by reference: each option referenced in a task's results in that option being added to the task's configuration form. In the option reference {{ options.foo_bar__required }}, the option key is foo_bar__required. The appearance and behavior of the option's form element is based on flags in in the option key – in this example, only the "required" flag is in use.

Mechanic flags provide only limited option validation. A task may define , by rendering error objects according to the task's its own validation logic.

1. Keys

Options are made available in the options variable, which is a hash having key-value pairs for each option key and option value. The option key must only contain ASCII numbers, lowercase letters, and underscores. The option key is reformatted for use as the option name presented to the user – underscores are replaced by spaces, and the entire line is sentence-cased.

Part
Rules
Example

Because option keys are registered via static analysis, options must each be referenced using a standard lookup (e.g. options.foobar) at least once.


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.


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) in additions to the general task options screen.

Flags fall into three categories:

Category
Purpose

3.1 Input‑type flags

Flag
UI control
Value returned
Key examples

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.

Choice/Select grammar

Use ordinal tokens to define values:

Ordinals (o1, o2, …) determine display order. Underscores inside values are preserved.

Range grammar

  • min<number> – required

  • max<number> – required

  • step<number> – optional (defaults to 1).

Picker grammar

  • Append _array for multi‑select.

  • Unsupported resources are rejected during validation.

3.2 Form‑modifier flags

Flag
Applies to
Effect

3.3 Auxiliary flags

Flag
Works with
Effect

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.

Custom rules? Learn more about .


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 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

Working with date options

Getting a plain string (strip the offset)

Converting to another timezone

file generators
docs
helpful article
<name>[__<flag>[ _<flag> ... ]]

name

Lowercase letters, numbers, and underscores only.

send_after

flag

One or more tokens that customise the field (see below).

required, date, picker_product

{% comment %}
  Option order:

  {{ options.api_key__required }}
  {{ options.mode__select_o1_test_o2_live }}
  {{ options.webhooks__array }}
{% endcomment %}

Input types

Choose a specific control and value type (date picker, slider, select list, …). Exactly one input‑type flag should be used.

Form modifiers

Fine‑tune how the control looks or validates (required, multiline, etc.).

Auxiliary flags

Extra behaviour for certain input types (future‑only dates, multi‑select choices, etc.).

(none - default)

Single‑line text

string

options.subject

multiline

Multiline text box

string

options.body__multiline

boolean

Checkbox

true/false

options.enabled__boolean

number

Numeric input (step=1)

number

options.count__number

code

Code‑formatted text area

string

options.script_snippet__code_multiline

keyval

Key → value repeater

hash

options.headers__keyval

array

Value repeater

array

options.tags__array

date

Calendar picker

"YYYY‑MM‑DD"

options.launch_date__date

datetime

Date+time picker

"YYYY‑MM‑DDTHH:MM"

options.send_at__datetime

time

Time‑only picker

"HH:MM"

options.quiet_time__time

color

Hex colour picker

"#RRGGBB"

options.theme_color__color

range_minX_maxY_stepZ

Slider + number box

number

options.qty__range_min0_max100_step5

select

Single‑choice dropdown

string

options.plan__select_o1_basic_o2_pro

choice

Radio buttons

string

options.tier__choice_o1_gold_o2_silver

multiselect

Checkbox list

array[string]

options.channels__multiselect_o1_email_o2_sms

picker_<resource>

Shopify resource picker

gid string

options.product__picker_product

picker_<resource>_array

Multi‑select resource picker

array[gid]

options.products__picker_product_array

<name>__select_o1_basic_o2_pro_o3_enterprise
               │  │     │  │
               │  │     │  └───── value #2
               │  │     └──────── ordinal marker
               │  └──────────── value #1
               └─────────────── ordinal marker
<name>__picker_<product|variant|collection>[_array]

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).

futureonly

date, datetime

Picker disallows past dates.

Text input

options.subject__required

"Welcome!"

Email input

options.reply_to__email_required

"[email protected]"

Checkbox

options.newsletter__boolean

false

Key–value map

options.headers__keyval

{ "X-Env": "staging" }

String list

options.tags__array

["vip","wholesale"]

0–100 slider

options.score__range_min0_max100

42

Colour picker

options.bg__color

"#336699"

Dropdown

options.plan__select_o1_basic_o2_pro

"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"

{{ 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
{{ 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
Tasks
code
custom validation
custom validation
environment variables
Logo
Task Requests | Mechanic

Email

The Email action is for sending email. ✅ It supports the store's , and supports attachments constructed by .

Restrictions

Mechanic sends email via , 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: .

Options

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.

If you're simply trying to add formatted text and aren't ready to dig into the code yourself, try using a tool like to quickly generate usable HTML.

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 .

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 [email protected].

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 .

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 .

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 }}.

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:

The template named "order_acknowledgement" could include the following Liquid, and get the expected results:

But, because order_data is a plain based entirely on JSON data, instead of being an enhanced order object (see ), the following Liquid usage would fail:

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 .

Examples

Simple

Using HTML

Using attachments

HTTP

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 .

When developing task code, verify your HTTP action's behavior with (making sure not to share sensitive information with this service).

Options

Option
Type
Notes

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 , 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.

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 and the "Basic" authentication type, use something like this:

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.

We recommend using an HTTPS proxy server (rather than HTTP or SOCKS5) for a secure connection between Mechanic and your proxy. is a good option for this kind of service.

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.

Result

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:

An HTTP action returns an object containing the following keys:

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 , use something like this:

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 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 .

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 action to display the response status, content type, and body.

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

...

Additional options may be provided, and will be made available to email templates as variables, named after each option

{% action "email" %}
  {
    "to": "[email protected]",
    "subject": "Thanks for your order!",
    "template": "order_acknowledgement",
    "order_data": {{ order | json }}
  }
{% endaction %}
This is the first item: {{ order_data.line_items.first.title }}
Remember order {{ order_data.customer.orders.any.first.number }}, your first ever?
{% action "email" %}
  {
    "to": "[email protected]",
    "subject": "Hello world",
    "body": "It's a mighty fine day!",
    "reply_to": {{ shop.customer_email | json }},
    "from_display_name": {{ shop.name | json }}
  }
{% endaction %}
{
  "action": {
    "type": "email",
    "options": {
      "to": "[email protected]",
      "subject": "Hello world",
      "body": "It's a mighty fine day!",
      "reply_to": "[email protected]",
      "from_display_name": "Example Store"
    }
  }
}
{% capture email_body %}
  <b>Hello!</b>

  It's fantastic to see you!
{% endcapture %}

{% action "email" %}
  {
    "to": "[email protected]",
    "subject": "Hello world",
    "body": {{ email_body | unindent | strip | newline_to_br | json }}
  }
{% endaction %}
{
  "action": {
    "type": "email",
    "options": {
      "to": "[email protected]",
      "subject": "Hello world",
      "body": "<b>Hello!</b><br />\n<br />\nIt's fantastic to see you!"
    }
  }
}
{% action "email" %}
  {
    "to": "[email protected]",
    "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 %}
{
  "action": {
    "type": "email",
    "options": {
      "to": "[email protected]",
      "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"
        }
      }
    }
  }
}
email templates
file generators
Postmark
"What are Transactional emails?"
wordtohtml.net
Uploading files to your website
Custom email domain
Email templates
hash
Environment variables
File generators

method

String, required

Must be one of "options", "head", "get", "post", "put", "patch", or "delete"

url

String, required

Must start with https:// or http://

body

String, required for non-GET requests

Format varies, see below

files

Hash, optional

May be set to a JSON object, mapping filenames to file generators

headers

Hash, optional

May be set to a JSON object, mapping header names to header values

follow_redirects

Boolean, optional

Defaults to true, may be set to false; controls whether or not 3xx responses with Location headers are automatically followed to their destination

proxy

String, optional

May be a proxy URI string beginning with https://, http://, or socks5://; see "Using a proxy" below

verify

Boolean, optional

May be set to false to disable SSL certificate verification

error_on_5xx

Boolean, optional

May be set to true to have 5xx HTTP response codes be considered action errors

{% action "http" %}
  {
    "method": "post",
    "url": "https://postman-echo.com/post",
    "body": {
      "hello": "world"
    },
    "files": {
      "robots.txt": {
        "url": "https://www.shopify.com/robots.txt"
      }
    }
  }
{% endaction %}
{% 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 %}
Example HTTP action using a proxy
{% action "http" %}
  {
    "method": "get",
    "url": "https://api.ipify.org?format=json",
    "proxy": "socks5://user:[email protected]:port"
  }
{% endaction %}

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

{% log response_type_header: action.run.result.headers['content-type'][0] %}
mechanic/user/text
mechanic/actions/perform
{% 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 %}
mechanic/actions/perform
webhook.site
file generators
the Authorization header
QuotaGuard Shield
Responding to action results
mechanic/actions/perform
decode_base64
Mechanic's retry policy
Echo

Shopify

The Shopify action sends requests to the Shopify admin API. It supports both REST and GraphQL requests.

Important Notice

Shopify is deprecating the Shopify Admin REST API which the Mechanic REST objects depend on. The first round of deprecations involve the product and variant endpoints. Read about the deprecation here and here. Use the GraphQL going forward. The product and variant objects will cease to work on on Feb 1, 2025 due to the changes being made by Shopify. Shopify will phase out the REST API completely over time, you can read more about this here.

All of our library tasks will be ported to use GraphQL only, which will provide a model for how you can update your custom tasks. You'll be able to update your non-customized library tasks with a click of a button ☺️ Please see these guides for migrating your custom tasks to GraphQL.

In Mechanic, writing data to Shopify must happen using an action. While the Shopify action is usually the right choice, the HTTP action can also be used for this purpose, by manually configuring authentication headers.

To learn more, see Interacting with Shopify.

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. In this style, a single GraphQL query string is supplied as the action options. The action tag has specific support for this action type, allowing this string to be provided as the contents of an action block.

To prepare complex query inputs, use the graphql_arguments Liquid filter.

{% action "shopify" %}
  mutation {
    customerCreate(
      input: {
        email: "[email protected]"
      }
    ) {
      customer {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": "\n  mutation {\n    customerCreate(\n      input: {\n        email: \"[email protected]\"\n      }\n    ) {\n      customer {\n        id\n      }\n      userErrors {\n        field\n        message\n      }\n    }\n  }\n"
  }
}

GraphQL with variables

This usage style invokes the Shopify GraphQL Admin API, and supports combining GraphQL queries with GraphQL 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

{% 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 %}
{
  "action": {
    "type": "shopify",
    "options": {
      "query": "\n  mutation DeleteProduct($productId: ID!) {\n    productDelete(\n      input: {\n        id: $productId\n      }\n    ) {\n      userErrors {\n        field\n        message\n      }\n    }\n  }\n",
      "variables": {
        "productId": "gid://shopify/Product/1234567890"
      }
    }
  }
}

Complex example

This example shows how the query and variables may be built up separately, and provided to the action using concise tag syntax.

{% 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 %}
{
  "action": {
    "type": "shopify",
    "options": {
      "query": "\n  mutation SetCustomerMetafield(\n    $customerId: ID!\n    $metafieldNamespace: String!\n    $metafieldKey: String!\n    $metafieldId: ID\n    $metafieldValue: String!\n  ) {\n    customerUpdate(\n      input: {\n        id: $customerId\n        metafields: [\n          {\n            id: $metafieldId\n            namespace: $metafieldNamespace\n            key: $metafieldKey\n            valueType: STRING\n            value: $metafieldValue\n          }\n        ]\n      }\n    ) {\n      userErrors {\n        field\n        message\n      }\n      customer {\n        metafield(\n          namespace: $metafieldNamespace\n          key: $metafieldKey\n        ){\n          id\n        }\n      }\n    }\n  }\n",
      "variables": {
        "customerId": "gid://shopify/Customer/700837494845",
        "metafieldNamespace": "test",
        "metafieldKey": "test",
        "metafieldId": "gid://shopify/Metafield/18788961353789",
        "metafieldValue": "1615244317"
      }
    }
  }
}

⚠️ Resourceful REST

Important Notice

Shopify is deprecating the Shopify Admin REST API which the Mechanic REST objects depend on. The first round of deprecations involve the product and variant endpoints. Read about the deprecation here and here. Use the GraphQL going forward. The product and variant objects will cease to work on on Feb 1, 2025 due to the changes being made by Shopify. Shopify will phase out the REST API completely over time, you can read more about this here.

All of our library tasks will be ported to use GraphQL only, which will provide a model for how you can update your custom tasks. You'll be able to update your non-customized library tasks with a click of a button ☺️ Please see these guides for migrating your custom tasks to GraphQL.

This usage style invokes the Shopify REST Admin API. It accepts an array of option values, containing these elements in order:

  1. Operation Must be one of "create" , "update" , or "delete" .

  2. Resource specification When creating, use a single string (e.g. "customer" ). When updating or deleting, use an array (e.g. ["customer", 123] ).

  3. An object of attributes Only applies to creating and updating.

Example: Creating a resource

This example creates a (minimal) customer record.

{% action "shopify" %}
  [
    "create",
    "customer",
    {
      "email": "[email protected]"
    }
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "create",
      "customer",
      {
        "email": "[email protected]"
      }
    ]
  }
}

Example: Updating a resource

This example appends a line to the order note (assuming a task subscription to shopify/orders/create).

{% action "shopify" %}
  [
    "update",
    [
      "order",
      {{ order.id | json }}
    ],
    {
      "note": {{ order.note | append: newline | append: newline | append: "We're adding a note! 💪" | strip | json }}
    }
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "update",
      [
        "order",
        3656038711357
      ],
      {
        "note": "[customer-supplied note]\n\nWe're adding a note! 💪"
      }
    ]
  }
}

Example: Deleting a resource

This example deletes a product, having a certain ID.

{% action "shopify" %}
  [
    "delete",
    ["product", 4814813560893]
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "delete",
      [
        "product",
        4814813560893
      ]
    ]
  }
}

⚠️ Explicit REST

Important Notice

Shopify is deprecating the Shopify Admin REST API which the Mechanic REST objects depend on. The first round of deprecations involve the product and variant endpoints. Read about the deprecation here and here. Use the GraphQL going forward. The product and variant objects will cease to work on on Feb 1, 2025 due to the changes being made by Shopify. Shopify will phase out the REST API completely over time, you can read more about this here.

All of our library tasks will be ported to use GraphQL only, which will provide a model for how you can update your custom tasks. You'll be able to update your non-customized library tasks with a click of a button ☺️ Please see these guides for migrating your custom tasks to GraphQL.

This usage style invokes Shopify REST Admin API. It accepts an array of option values, containing these elements in order:

  1. Operation Must be one of "get", "post" , "put" , or "delete"

  2. Request path The entire, literal request path to use, including the requested API version — e.g. "/admin/api/2020-01/orders.json"

  3. A JSON object of attributes In general, this means a wrapper object whose key is named after the current resource type, and whose value is the same set of data that would be used in the resourceful style

When switching from resourceful to explicit REST, it's common to forget the outer wrapper object. This wrapper is required by Shopify for all request methods except GET and DELETE; it's handled automatically during resourceful usage, but must be handled manually during explicit usage.

Example: Creating a resource

This example creates a (minimal) customer record.

{% action "shopify" %}
  [
    "post",
    "/admin/api/2020-01/customers.json",
    {
      "customer": {
        "email": "[email protected]"
      }
    }
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "post",
      "/admin/api/2020-01/customers.json",
      {
        "customer": {
          "email": "[email protected]"
        }
      }
    ]
  }
}

Example: Updating a resource

This example appends a line to the order note (assuming a task subscription to shopify/orders/create).

{% action "shopify" %}
  [
    "put",
    "/admin/api/2020-01/orders/{{ order.id }}.json",
    {
      "order": {
        "note": {{ order.note | append: newline | append: newline | append: "We're adding a note! 💪" | strip | json }}
      }
    }
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "put",
      "/admin/api/2020-01/orders/3656063189053.json",
      {
        "order": {
          "note": "We're adding a note! 💪"
        }
      }
    ]
  }
}

Example: Deleting a resource

This example deletes a product, having a certain ID.

{% action "shopify" %}
  [
    "delete",
    "/admin/api/2020-01/products/4814813724733.json"
  ]
{% endaction %}
{
  "action": {
    "type": "shopify",
    "options": [
      "delete",
      "/admin/api/2020-01/products/4814813724733.json"
    ]
  }
}
Logo