Save Mechanic form submissions to Shopify metaobjects, with Mechanic.

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

Save Mechanic form submissions to Shopify metaobjects

Save answers from Mechanic form submissions as Shopify metaobject entries, with storefront access disabled and attachment details for reference.

Runs Occurs when a user manually triggers the task and Occurs whenever user/forms/submission is triggered. Configuration includes webhook event topic, form, and form name.

15-day free trial – unlimited tasks

Documentation

Save answers from Mechanic form submissions as Shopify metaobject entries, with storefront access disabled and attachment details for reference.

These tasks are for Mechanic's form builder, not Shopify Forms. Submissions use your ordinary webhook, pass through AWS, and run subscribed tasks in the background. Installing this task does not create or publish a form, change its webhook, or place a theme block.

Connect a form

  1. In the form's Submission settings, choose a webhook. Copy its exact event topic into Webhook event topic in this task; the example user/forms/submission is not automatically connected.
  2. Optionally choose a published form in Form to save only that form. Leave it blank to handle any Mechanic form on this webhook. Form IDs and submission IDs identify visitor input, not authenticated customers.
  3. Complete the destination setup below, save and enable the task, then refresh the form's connected tasks. Disabled tasks do not save submissions.
  4. Send a test from the actual storefront, then check the resulting task action and destination. The builder and theme-editor previews do not send anything. The form's confirmation does not guarantee that a storage action succeeded.

Both ordinary and full-request webhook data modes work. Scalar answers, including numbers and checkbox values, remain strings; multiple-choice arrays and addresses retain their structure. Hidden questions omitted by the form are not saved. The task rejects malformed values and answers/file details larger than 100 KB; file contents do not count toward that task storage limit because they are not copied.

Uploaded files

This task saves file details, not uploaded files. It records each attachment's field key, name, reported MIME type and size, and contents_saved: false. It never stores base64 contents in a sheet or metaobject, uploads documents to Shopify Files, or publishes download links. The original file remains in the ordinary Mechanic event and expires with that event's retention. Install or write a separate file-archiving task if files must be kept longer. File names, MIME types and sizes are visitor-supplied details, not verified content.

Moving shops or stopping

Set up the destination and reconnect the new shop's webhook when moving this task or importing a form. Imported forms have new IDs. Publish the imported form, then select it again in the Form dropdown if this task filters to one form. Disabling the task stops future saves and leaves existing records and the shared webhook intact. Other subscribed tasks can still run.

Shopify setup

  1. Optionally set Form name to a short recognizable name, such as Warranty request. It labels entries; it does not choose or authenticate the form. If blank, the form code is used.
  2. Save the task and grant its requested Shopify metaobject permissions. Run it manually once to create the Mechanic form submission definition (mechanic_form_submission). If a compatible definition already exists with storefront access off, the task leaves it unchanged.
  3. After that setup action succeeds, submissions create entries under Content → Metaobjects → Mechanic form submission in Shopify. Each entry contains the form code/revision, submission and Mechanic event IDs, received time, all answers as JSON, and file details as JSON. This generic structure supports changing questions without a new metaobject definition for every form.

The task checks the existing definition before writing, including on reruns. It refuses definitions with storefront access enabled, incompatible field types, or extra required fields. It never automatically changes an existing definition. Keep storefront access disabled: these entries may contain customer information. Merchant-owned entries remain accessible to authorized Shopify staff and apps; this is not a private file vault.

Retries and duplicates

The entry handle is derived from the Mechanic event ID. Rerunning the same event updates the same entry instead of creating another. The browser-supplied submission ID is stored only for reference; it cannot select and overwrite a previous entry. A second HTTP submission creates a different Mechanic event and therefore a separate entry, even if its browser submission ID is the same. Review those as possible duplicates.

This task does not link records to a customer or order based on visitor-supplied IDs. Extend its code deliberately if you need authenticated associations or mappings into a different metaobject definition. Shopify's record limits and permissions still apply; inspect failed actions before treating a submission as saved.

See Shopify metaobject entries.

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
mechanic/user/trigger
{{ 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) , form name
Code
{% comment %}
  {{ options.webhook_event_topic__required }}
  {{ options.form__storefrontform }}
  {{ options.form_name }}
{% endcomment %}
{% if event.topic != "mechanic/user/trigger" and event.topic != options.webhook_event_topic__required %}{% break %}{% endif %}
{% unless event.topic == "mechanic/user/trigger" %}
{% 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 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 %}
    {% 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. No submission data was 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 %}
{% endunless %}
{% capture query %}
  query {
    metaobjectDefinitionByType(type: "mechanic_form_submission") {
      id
      access { storefront }
      fieldDefinitions { key type { name } required }
    }
  }
{% endcapture %}
{% assign result = query | shopify %}
{% if event.preview %}
  {% capture fixture %}
    {"data":{"metaobjectDefinitionByType":{% if event.topic == "mechanic/user/trigger" %}null{% else %}{"id":"gid://shopify/MetaobjectDefinition/1","access":{"storefront":"NONE"},"fieldDefinitions":[{"key": "title", "type": {"name": "single_line_text_field"}, "required": false}, {"key": "form_id", "type": {"name": "single_line_text_field"}, "required": false}, {"key": "form_revision", "type": {"name": "single_line_text_field"}, "required": false}, {"key": "submission_id", "type": {"name": "single_line_text_field"}, "required": false}, {"key": "event_id", "type": {"name": "single_line_text_field"}, "required": false}, {"key": "received_at", "type": {"name": "date_time"}, "required": false}, {"key": "answers", "type": {"name": "json"}, "required": false}, {"key": "attachments", "type": {"name": "json"}, "required": false}]}{% endif %}}}
  {% endcapture %}
  {% assign result = fixture | parse_json %}
{% endif %}
{% if result.errors != blank or result.data == nil %}
  {% error "The metaobject definition could not be checked. Nothing was saved." %}{% break %}
{% endif %}
{% assign definition = result.data.metaobjectDefinitionByType %}
{% capture expected_json %}{"title": "single_line_text_field", "form_id": "single_line_text_field", "form_revision": "single_line_text_field", "submission_id": "single_line_text_field", "event_id": "single_line_text_field", "received_at": "date_time", "answers": "json", "attachments": "json"}{% endcapture %}
{% assign expected = expected_json | parse_json %}
{% if definition %}
  {% if definition.access.storefront != "NONE" %}
    {% error "This submission definition has storefront access. Turn off storefront access before saving customer submissions." %}{% break %}
  {% endif %}
  {% assign compatible = true %}
  {% for expected_field in expected %}
    {% assign actual = definition.fieldDefinitions | where: "key", expected_field[0] | first %}
    {% if actual.type.name != expected_field[1] %}{% assign compatible = false %}{% endif %}
  {% endfor %}
  {% for actual in definition.fieldDefinitions %}
    {% if actual.required and expected[actual.key] == nil %}{% assign compatible = false %}{% endif %}
  {% endfor %}
  {% unless compatible %}
    {% error "The mechanic_form_submission definition has incompatible fields. Review its fields before running this task; the task does not change existing definitions." %}{% break %}
  {% endunless %}
{% endif %}
{% if event.topic == "mechanic/user/trigger" %}
  {% if definition %}
    {% log "The Mechanic form submission definition is ready. No changes were needed." %}
  {% else %}
    {% action "shopify" %}
      {"query":"mutation SetupFormSubmissions($definition: MetaobjectDefinitionCreateInput!) { metaobjectDefinitionCreate(definition: $definition) { metaobjectDefinition { id } userErrors { field message } } }","variables":{"definition":{"name": "Mechanic form submission", "type": "mechanic_form_submission", "displayNameKey": "title", "access": {"storefront": "NONE"}, "fieldDefinitions": [{"key": "title", "name": "Submission", "type": "single_line_text_field"}, {"key": "form_id", "name": "Form code", "type": "single_line_text_field"}, {"key": "form_revision", "name": "Form revision", "type": "single_line_text_field"}, {"key": "submission_id", "name": "Submission ID", "type": "single_line_text_field"}, {"key": "event_id", "name": "Mechanic event ID", "type": "single_line_text_field"}, {"key": "received_at", "name": "Received at", "type": "date_time"}, {"key": "answers", "name": "Answers", "type": "json"}, {"key": "attachments", "name": "File details (contents not saved)", "type": "json"}]}}}
    {% endaction %}
  {% endif %}
  {% break %}
{% endif %}
{% unless definition %}
  {% error "Run this task manually once to create the Mechanic form submission definition before collecting submissions." %}{% break %}
{% endunless %}
{% comment %}Use the trusted Mechanic event ID, never a visitor-controlled ID, to select the stored record.{% endcomment %}
{% assign handle = event_id | sha256 | prepend: "event-" %}
{% assign title = options.form_name | default: payload.form.id | append: " — " | append: received_at %}
{% action "shopify" %}
  {"query":"mutation SaveFormSubmission($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) { metaobjectUpsert(handle: $handle, metaobject: $metaobject) { metaobject { id handle } userErrors { field message } } }",
   "variables":{"handle":{"type":"mechanic_form_submission","handle":{{ handle | json }}},"metaobject":{"fields":[
     {"key":"title","value":{{ title | json }}},
     {"key":"form_id","value":{{ payload.form.id | json }}},
     {"key":"form_revision","value":{{ revision | json }}},
     {"key":"submission_id","value":{{ payload.submission_id | json }}},
     {"key":"event_id","value":{{ event_id | json }}},
     {"key":"received_at","value":{{ received_at | json }}},
     {"key":"answers","value":{{ answers_json | json }}},
     {"key":"attachments","value":{{ attachments_json | json }}}
   ]}}}
{% 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