Email Mechanic form submissions, with Mechanic.

Mechanic is a development and ecommerce automation platform for Shopify. :)

Email Mechanic form submissions

Email the answers from a Mechanic form submission to your team, optionally including uploaded files as attachments.

Runs Occurs whenever user/forms/submission is triggered. Configuration includes webhook event topic, form, email recipients, email subject, include uploaded files, and reply to email field key.

15-day free trial – unlimited tasks

Documentation

Email the answers from a Mechanic form submission to your team, optionally including uploaded files as attachments.

This task is for Mechanic's form builder. It uses the form's ordinary webhook and runs in the background alongside any Google Sheets, metaobject or other tasks subscribed to that topic. It does not create or publish a form, change its webhook, or send a message synchronously to the visitor.

Setup

  1. In the form's Submission settings, choose a webhook. Copy its exact event topic into Webhook event topic; the example user/forms/submission is not automatically connected.
  2. Optionally choose a published form in Form to receive only that form. Leave it blank to handle any Mechanic form on the selected webhook.
  3. Set Email recipients to the team members who should receive submissions, and choose an Email subject. Recipients come from task settings, never from the visitor's answers.
  4. Optionally enable Include uploaded files. Optionally set Reply to email field key to a field such as email, so replying to the notification addresses the submitter. An omitted, empty or invalid answer does not set Reply-To. The supplied address is not proof of identity.
  5. Save and enable the task. Your Mechanic account must be approved to send email. Submit a test from the actual storefront and check the email action and recipient inbox. The preview inside Mechanic and the form inside Shopify’s theme editor do not send email. A storefront opened in a separate tab, including an unpublished theme preview, sends real submissions and can run this task. A form confirmation means the request was accepted; email delivery happens afterward and may fail independently.

Answers and attachments

The email includes every submitted answer, identified by its form field data key, plus form/revision, submission/event IDs and received time. Arrays and addresses are shown as JSON. Hidden questions omitted by the form are not included; empty submissions still produce a notification. Answer text is HTML-escaped. Both ordinary and full-request webhook data modes work.

Include uploaded files is off by default. When enabled, files arrive as real email attachments using Mechanic's existing file generators; no Google connection or Shopify Files upload is required. Filenames are made safe and prefixed with the field key to avoid collisions. Original filenames appear in the message. When disabled, the email contains only file names and sizes, with no download links. Enable this option to receive the files by email. For lasting storage, also use the Google Sheets task with its Google Drive upload option on the same webhook topic. Files in the original event remain subject to normal event retention; this email task does not host files or create temporary download links.

Attachments may total at most 3 MB (3,145,728 bytes) per submission. The task verifies base64 structure and byte counts before creating the email action. It also bounds the number/shape of fields and the combined answer/file-detail data to 100 KB. A malformed or oversized submission produces a task error and no email. File names and types are visitor-supplied; this task does not scan attachments.

This task sends a notification to the configured team. It does not automatically email a receipt to the submitter. The standard Mechanic sender and email template settings apply. You can use it alongside the Google Sheets task and metaobject task.

Replays and stopping

Replaying an event or retrying an email after an uncertain response can send another notification. The IDs help correlate messages; they do not guarantee delivery exactly once. Disabling this task stops future notifications and leaves the shared webhook and other subscribed tasks intact. When moving shops, configure recipients, email approval, and the new webhook topic again. If this task filters to one form, publish the imported form and select it in the Form dropdown.

See Mechanic's Email action.

Developer details

Mechanic is designed to benefit everybody: merchants, customers, developers, agencies, Shopifolks, everybody.

That’s why we make it easy to configure automation without code, why we make it easy to tweak the underlying code once tasks are installed, and why we publish it all here for everyone to learn from.

(By the way, have you seen our documentation? Have you joined the Slack community?)

Open source
View on GitHub to contribute to this task
Subscriptions
{{ options.webhook_event_topic__required }}
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
webhook event topic (required) , form (storefrontform) , email recipients (email, array, required) , email subject (required) , include uploaded files (boolean) , reply to email field key
Code
{% comment %}
  {{ options.webhook_event_topic__required }}
  {{ options.form__storefrontform }}
  {{ options.email_recipients__email_array_required }}
  {{ options.email_subject__required }}
  {{ options.include_uploaded_files__boolean }}
  {{ options.reply_to_email_field_key }}
{% endcomment %}
{% if event.topic != options.webhook_event_topic__required %}{% break %}{% endif %}
{% assign forward_files = options.include_uploaded_files__boolean %}
{% assign payload = event.data %}
{% if payload.body != nil %}{% assign payload = payload.body %}{% endif %}
{% if event.preview %}
  {% capture sample %}
    {"form":{"id":{{ options.form__storefrontform | default: "11111111-1111-4111-8111-111111111111" | json }},"revision":"1"},"submission_id":"22222222-2222-4222-8222-222222222222","fields":{"name":"Pat Example","email":"pat@example.com","product":"Garden lamp","issue":"The switch stopped working.","agree":"false","receipt":{"name":"receipt.txt","mime_type":"text/plain","size":4,"content_base64":"dGVzdA=="}}}
  {% endcapture %}
  {% assign payload = sample | parse_json %}
{% endif %}
{% assign form_keys = payload.form | keys %}
{% assign payload_keys = payload | keys %}
{% assign submitted_fields = payload.fields %}
{% unless payload_keys contains "fields" %}
  {% comment %}Empty optional uploads and checkbox groups produce no multipart field parts.{% endcomment %}
  {% assign submitted_fields = hash %}
{% endunless %}
{% assign field_keys = submitted_fields | keys %}
{% unless form_keys and field_keys %}
  {% error "This event is not a form submission. Expected form and fields objects in the webhook body." %}
  {% break %}
{% endunless %}
{% assign form_id_type = payload.form.id | json | slice: 0, 1 %}
{% assign submission_id_type = payload.submission_id | json | slice: 0, 1 %}
{% unless form_id_type == '"' and submission_id_type == '"' %}
  {% error "The form and submission IDs must be strings." %}{% break %}
{% endunless %}
{% assign valid_form_id = payload.form.id | match: '\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z' %}
{% assign valid_submission_id = payload.submission_id | match: '\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z' %}
{% unless valid_form_id and valid_submission_id %}
  {% error "The form or submission ID is invalid." %}{% break %}
{% endunless %}
{% if options.form__storefrontform != blank and options.form__storefrontform != payload.form.id %}
  {% log "This submission belongs to another form; nothing was saved." %}{% break %}
{% endif %}
{% assign revision = payload.form.revision | append: "" %}
{% assign valid_revision = revision | match: '\A[1-9][0-9]{0,9}\z' %}
{% unless valid_revision %}
  {% error "The form revision is missing or invalid." %}{% break %}
{% endunless %}
{% if field_keys.size > 50 %}
  {% error "A form submission may have at most 50 fields." %}{% break %}
{% endif %}
{% assign event_id = event.id | append: "" %}
{% assign received_at = event.created_at | date: "%Y-%m-%dT%H:%M:%S%:z" %}
{% if event.preview %}
  {% assign event_id = "33333333-3333-4333-8333-333333333333" %}
  {% assign received_at = "2026-09-14T12:00:00+00:00" %}
{% endif %}
{% if event_id == blank or received_at == blank %}
  {% error "The Mechanic event ID and creation time are required." %}{% break %}
{% endif %}
{% assign answers = hash %}
{% assign attachments = hash %}
{% assign invalid_field = false %}
{% assign generated_files = hash %}
{% assign file_names = hash %}
{% assign total_file_bytes = 0 %}
{% assign address_keys = "line1,line2,city,region,postal_code,country" | split: "," %}
{% for field in submitted_fields %}
  {% assign key = field[0] %}
  {% assign value = field[1] %}
  {% assign key_valid = key | match: '\A[a-z][a-z0-9_]{0,63}\z' %}
  {% unless key_valid %}{% assign invalid_field = true %}{% break %}{% endunless %}
  {% assign value_type = value | json | slice: 0, 1 %}
  {% if value_type == '"' %}
    {% if value.size > 4000 %}{% assign invalid_field = true %}{% break %}{% endif %}
    {% assign answers[key] = value %}
  {% elsif value_type == "[" %}
    {% if value.size > 50 %}{% assign invalid_field = true %}{% break %}{% endif %}
    {% for item in value %}
      {% assign item_type = item | json | slice: 0, 1 %}
      {% if item_type != '"' or item.size > 4000 %}{% assign invalid_field = true %}{% break %}{% endif %}
    {% endfor %}
    {% assign answers[key] = value %}
  {% elsif value_type == "{" %}
    {% if value.content_base64 != nil %}
      {% assign name_type = value.name | json | slice: 0, 1 %}
      {% assign mime_type = value.mime_type | json | slice: 0, 1 %}
      {% assign content_type = value.content_base64 | json | slice: 0, 1 %}
      {% assign size_json = value.size | json %}
      {% assign size_valid = size_json | match: '\A[0-9]+\z' %}
      {% if name_type != '"' or mime_type != '"' or content_type != '"' or size_valid == nil or value.size > 4194304 or value.name.size > 255 or value.mime_type.size > 255 %}
        {% assign invalid_field = true %}{% break %}
      {% endif %}
      {% assign file = hash %}
      {% assign file["name"] = value.name %}
      {% assign file["mime_type"] = value.mime_type %}
      {% assign file["size"] = value.size %}
      {% assign file["contents_saved"] = false %}
      {% assign attachments[key] = file %}
      {% if forward_files %}
        {% if value.content_base64.size > 4194304 %}{% assign invalid_field = true %}{% break %}{% endif %}
        {% assign valid_base64 = value.content_base64 | match: '\A[A-Za-z0-9+/]*={0,2}\z' %}
        {% assign remainder = value.content_base64.size | modulo: 4 %}
        {% unless valid_base64 and remainder == 0 %}{% assign invalid_field = true %}{% break %}{% endunless %}
        {% assign decoded_bytes = value.content_base64.size | divided_by: 4 | times: 3 %}
        {% assign tail = value.content_base64 | slice: -2, 2 %}
        {% if tail == "==" %}
          {% assign decoded_bytes = decoded_bytes | minus: 2 %}
        {% elsif tail contains "=" %}
          {% assign decoded_bytes = decoded_bytes | minus: 1 %}
        {% endif %}
        {% assign total_file_bytes = total_file_bytes | plus: decoded_bytes %}
        {% if decoded_bytes != value.size or total_file_bytes > 3145728 %}{% assign invalid_field = true %}{% break %}{% endif %}
        {% assign safe_name = value.name | scan: '[A-Za-z0-9._-]' | join: "" | slice: 0, 120 | default: "upload.bin" %}
        {% assign safe_name = key | append: "-" | append: safe_name %}
        {% assign generator = hash %}
        {% assign generator["base64"] = value.content_base64 %}
        {% assign generated_files[safe_name] = generator %}
        {% assign file_names[key] = safe_name %}
      {% endif %}
    {% else %}
      {% for part in value %}
        {% assign part_type = part[1] | json | slice: 0, 1 %}
        {% unless address_keys contains part[0] %}{% assign invalid_field = true %}{% break %}{% endunless %}
        {% if part_type != '"' or part[1].size > 200 %}{% assign invalid_field = true %}{% break %}{% endif %}
      {% endfor %}
      {% assign answers[key] = value %}
    {% endif %}
  {% else %}
    {% assign invalid_field = true %}
  {% endif %}
  {% if invalid_field %}{% break %}{% endif %}
{% endfor %}
{% if invalid_field %}
  {% error "A field has an unsupported value or exceeds the form limits. Files being saved or attached must contain valid base64, match their reported size, and total at most 3 MB. Nothing was sent or saved." %}{% break %}
{% endif %}
{% assign answers_json = answers | json %}
{% assign attachments_json = attachments | json %}
{% assign encoded_size = answers_json | append: attachments_json | base64 | size %}
{% if encoded_size > 133332 %}
  {% error "The answers and file details exceed this task's 100 KB storage limit. Nothing was saved." %}{% break %}
{% endif %}

{% assign reply_to = nil %}
{% if options.reply_to_email_field_key != blank %}
  {% assign candidate = answers[options.reply_to_email_field_key] %}
  {% assign candidate_type = candidate | json | slice: 0, 1 %}
  {% if candidate_type == '"' and candidate != blank %}
    {% assign candidate = candidate | strip %}
    {% assign valid_email = candidate | match: '\A[^\s@<>,;:]+@[^\s@<>,;:]+\.[^\s@<>,;:]+\z' %}
    {% if valid_email and candidate.size <= 254 %}
      {% assign reply_to = candidate %}
    {% else %}
      {% log "The submitted reply-to address was invalid; the notification will be sent without Reply-To." %}
    {% endif %}
  {% endif %}
{% endif %}
{% capture body %}
  <h2>New form submission</h2>
  <table>
    {% for answer in answers %}
      {% assign value_type = answer[1] | json | slice: 0, 1 %}
      {% assign display_value = answer[1] %}
      {% if value_type == "[" or value_type == "{" %}{% assign display_value = answer[1] | json %}{% endif %}
      <tr><th align="left" valign="top">{{ answer[0] | escape }}</th><td>{{ display_value | escape | newline_to_br }}</td></tr>
    {% else %}
      <tr><td>No answers were provided.</td></tr>
    {% endfor %}
  </table>
  {% if attachments.size > 0 %}
    <h3>Uploaded files</h3>
    {% if forward_files %}<p>The files are attached to this email.</p>{% else %}<p>File details only. Uploaded files are not attached.</p>{% endif %}
    <ul>{% for file in attachments %}<li>{{ file[0] | escape }}: {{ file[1].name | escape }} ({{ file[1].size }} bytes)</li>{% endfor %}</ul>
  {% endif %}
  <p>Received: {{ received_at | escape }}<br>Form code: {{ payload.form.id | escape }}<br>Revision: {{ revision | escape }}<br>Submission ID: {{ payload.submission_id | escape }}<br>Mechanic event ID: {{ event_id | escape }}</p>
{% endcapture %}
{% action "email", __perform_event: false %}
  {"to":{{ options.email_recipients__email_array_required | json }},"subject":{{ options.email_subject__required | json }},"body":{{ body | strip | json }}{% if reply_to %},"reply_to":{{ reply_to | json }}{% endif %}{% if forward_files and generated_files.size > 0 %},"attachments":{{ generated_files | json }}{% endif %}}
{% endaction %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Webhook event topic
user/forms/submission
Email subject
New form submission