Copy product metafields to each product's tags, with Mechanic.

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

Copy product metafields to each product's tags

This task exists to fill the gap between Shopify's admin product search, and product metafields. Use this task to copy metafield values to product tags, allowing you to filter products by tags based on those metafields.

Runs Occurs when a user manually triggers the task. Configuration includes metafield namespace, metafield keys and tag prefixes, monitor new and updated products, remove outdated prefixed tags, and test mode.

15-day free trial – unlimited tasks

Documentation

This task exists to fill the gap between Shopify's admin product search, and product metafields. Use this task to copy metafield values to product tags, allowing you to filter products by tags based on those metafields.

Begin by entering in the namespace for the metafield(s) you will be configuring for this task. Then, for each metafield, click Add item, add the exact metafield type on the left, and optionally a prefix to apply to tags generated for that specific metafield type. The tag prefix should include any desired demarcation (e.g. spaces, dashes, colons) from the metafield value.

To have the task remove prefixed tags that no longer apply, then enable the Remove outdated prefixed tags option. As an example: if a product has a "Released: 2021-10-01" tag, and this task is configured with a "Released: " tag prefix, and the associated metafield value changes (or is cleared), then the task can remove the original tag.

When run manually, this task scans your entire product catalog. Optionally, you can enable the Monitor new and updated products option to have it also process products as they are created and updated.

It is highly recommended that you first run this task in Test mode and review the task log to see what tags will be set based on your configuration settings, the metafield data, and the task logic for handling each type.

This task supports the following Shopify metafield types: boolean, color, date, date_time, dimension, number_decimal, number_integer, rating, single_line_text_field, volume, and weight. Additionally, list types are supported for all of the above fields except for boolean.

Important Notes:
- Be sure to run a full manual scan when new metafield keys are added in this task, so that all of your products can be evaluated with the tagging logic.
- This task cannot remove outdated tags without prefixes, as it will not be able to identify the outdated values (Shopify does not have any metadata or authorship attached to product tags).

- This task only supports a single metafield namespace. If you want to use additional namespaces, then multiple instances of this task can be implemented, each configured with a distinct namespace. Caution: do not use the same tag prefix in multiple instances of this task as then each instance will respond to the other's add/remove tagging actions infinitely.
- This task is mildly opinionated about the formatting of tags based on the metafield type (e.g. dimension metafields will concatenate the value and dimensional unit together with a space.). Each type is broken out within the task code, allowing for easy customization by type.

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
{% if options.monitor_new_and_updated_products__boolean %}
  shopify/products/update
{%endif%}
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
metafield namespace (required), metafield keys and tag prefixes (keyval, required), monitor new and updated products (boolean), remove outdated prefixed tags (boolean), test mode (boolean)
Code
{% comment %}
  An opinion about object order:

  {{ options.metafield_namespace__required }}
  {{ options.metafield_keys_and_tag_prefixes__keyval_required }}
  {{ options.monitor_new_and_updated_products__boolean }}
  {{ options.remove_outdated_prefixed_tags__boolean }}
  {{ options.test_mode__boolean }}
{% endcomment %}

{% assign metafield_namespace = options.metafield_namespace__required %}
{% assign metafield_keys_and_tag_prefixes = options.metafield_keys_and_tag_prefixes__keyval_required %}
{% assign metafield_keys = metafield_keys_and_tag_prefixes | keys %}

{% assign metafields = array %}
{% assign tag_prefixes_hash = hash %}

{% for keyval in metafield_keys_and_tag_prefixes %}
  {% assign metafield_key = keyval[0] %}
  {% assign tag_prefix = keyval[1] %}

  {% if metafield_key != blank %}
    {% capture metafield %}
      {{ metafield_key }}: metafield(
        namespace: {{ metafield_namespace | json }}
        key: {{ metafield_key | json }}
      ) {
        type
        value
      }
    {% endcapture %}

    {% assign metafields = metafields | push: metafield %}

    {% if tag_prefix != blank %}
      {% assign tag_prefixes_hash[metafield_key] = tag_prefix | lstrip %}
    {% endif %}
  {% endif %}
{% endfor %}

{% assign tag_prefixes = tag_prefixes_hash | values %}

{% assign products = array %}

{% if event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %}
  {% assign cursor = nil %}

  {% for n in (0..1000) %}
    {% capture query %}
      query {
        products(
          first: 25
          after: {{ cursor | json }}
        ) {
          pageInfo {
            hasNextPage
          }
          edges {
            cursor
            node {
              id
              tags
              {{ metafields | join: newline }}
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% assign products_batch = result.data.products.edges | map: "node" %}

    {% assign products = products | concat: products_batch %}

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

{% elsif event.topic contains "shopify/products/" %}
  {% capture query %}
    query {
      product(id: {{ product.admin_graphql_api_id | json }}) {
        id
        tags
        {{ metafields | join: newline }}
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% assign products[0] = result.data.product %}
{% endif %}

{% if event.preview %}
  {% capture products_json %}
    [
      {
        "id": "gid://shopify/Product/1234567890",
        "tags": [
          {{ tag_prefixes.first | append: "sample" | json }}
        ],
        {{ metafield_keys.first | json }}: {
          "type": "single_line_text_field",
          "value": "Lorem ipsum"
        }
      }
    ]
  {% endcapture %}

  {% assign products = products_json | parse_json %}
{% endif %}

{% assign product_ids_and_tags = hash %}

{% for product in products %}
  {% assign tags_should_have = array %}
  {% assign tags_to_add = array %}
  {% assign tags_to_remove = array %}

  {% unless event.preview %}
    {% log product_before_tagging_updates: product %}
  {% endunless %}

  {% for metafield_key in metafield_keys %}
    {% assign tag_prefix = tag_prefixes_hash[metafield_key] %}
    {% assign product_metafield = product[metafield_key] %}

    {% if product_metafield != blank %}
      {% if product_metafield.type contains "list." %}
        {% assign product_metafield_values = product_metafield.value | parse_json %}
        {% assign product_metafield_type = product_metafield.type | remove: "list." %}
      {% else %}
        {% assign product_metafield_values = array | push: product_metafield.value %}
        {% assign product_metafield_type = product_metafield.type %}
      {% endif %}

      {% case product_metafield_type %}
        {% when "boolean" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "color" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "date" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "date_time" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "dimension" %}
          {% for product_metafield_value in product_metafield_values %}
            {% assign parsed_product_metafield_value = product_metafield_value | parse_json %}
            {% capture tag %}{{ tag_prefix }}{{ parsed_product_metafield_value.value }} {{ parsed_product_metafield_value.unit }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "number_decimal" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "number_integer" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "rating" %}
          {% for product_metafield_value in product_metafield_values %}
            {% assign parsed_product_metafield_value = product_metafield_value | parse_json %}
            {% capture tag %}{{ tag_prefix }}{{ parsed_product_metafield_value.value }} / {{ parsed_product_metafield_value.scale_max }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "single_line_text_field" %}
          {% for product_metafield_value in product_metafield_values %}
            {% capture tag %}{{ tag_prefix }}{{ product_metafield_value }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "volume" %}
          {% for product_metafield_value in product_metafield_values %}
            {% assign parsed_product_metafield_value = product_metafield_value | parse_json %}
            {% capture tag %}{{ tag_prefix }}{{ parsed_product_metafield_value.value }} {{ parsed_product_metafield_value.unit }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% when "weight" %}
          {% for product_metafield_value in product_metafield_values %}
            {% assign parsed_product_metafield_value = product_metafield_value | parse_json %}
            {% capture tag %}{{ tag_prefix }}{{ parsed_product_metafield_value.value }} {{ parsed_product_metafield_value.unit }}{% endcapture %}
            {% assign tags_should_have[tags_should_have.size] = tag | strip %}
          {% endfor %}

        {% else %}
          {% log
            message: "Unsupported metafield type for this task",
            metafield_type: product_metafield.type,
            product_id: product.id
          %}
      {% endcase %}
    {% endif %}
  {% endfor %}

  {% if options.remove_outdated_prefixed_tags__boolean %}
    {% for tag_prefix in tag_prefixes %}
      {% assign tag_prefix_size = tag_prefix.size %}

      {% for product_tag in product.tags %}
        {% assign product_tag_slice = product_tag | slice: 0, tag_prefix_size %}

        {% if product_tag.size > tag_prefix_size and product_tag_slice == tag_prefix %}
          {% unless tags_should_have contains product_tag %}
            {% assign tags_to_remove = tags_to_remove | push: product_tag %}
          {% endunless %}
        {% endif %}
      {% endfor %}
    {% endfor %}
  {% endif %}

  {% for tag_should_have in tags_should_have %}
    {% unless product.tags contains tag_should_have %}
      {% assign tags_to_add = tags_to_add | push: tag_should_have %}
    {% endunless %}
  {% endfor %}

  {% if tags_to_add != blank or tags_to_remove != blank %}
    {% assign product_ids_and_tags[product.id] = hash %}
    {% assign product_ids_and_tags[product.id]["tags_to_add"] = tags_to_add %}
    {% assign product_ids_and_tags[product.id]["tags_to_remove"] = tags_to_remove %}

  {% else %}
    {% log
      message: "No tagging operations needed for this product; skipping.",
      product_id: product.id
    %}
  {% endif %}
{% endfor %}

{% if options.test_mode__boolean %}
  {% action "echo" %}
    {
      "message": "Found {{ product_ids_and_tags.size }} tagging operations",
      "product_ids_and_tags": {{ product_ids_and_tags | json }}
    }
  {% endaction %}

{% else %}
  {% for keyval in product_ids_and_tags %}
    {% assign product_id = keyval[0] %}
    {% assign tags_to_add = keyval[1].tags_to_add %}
    {% assign tags_to_remove = keyval[1].tags_to_remove %}

    {% if tags_to_add != blank or tags_to_remove != blank %}
      {% action "shopify" %}
        mutation {
          {% if tags_to_add != blank %}
            tagsAdd(
              id: {{ product_id | json }}
              tags: {{ tags_to_add | json }}
            ) {
              userErrors {
                field
                message
              }
            }
          {% endif %}
          {% if tags_to_remove != blank %}
            tagsRemove(
              id: {{ product_id | json }}
              tags: {{ tags_to_remove | json }}
            ) {
              userErrors {
                field
                message
              }
            }
          {% endif %}
        }
      {% endaction %}
    {% endif %}
  {% endfor %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more