Auto create collections by metafield values, with Mechanic.

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

Auto create collections by metafield values

Running on a schedule or manually, this task will create automated collections based on the validation values of the configured product metafield definitions. Additionally, configuring one or more exact sales channel names will enable publishing of any newly created collections by this task to those sales channels.

Runs Occurs when a user manually triggers the task and Occurs when a Mechanic action is performed. Configuration includes product metafields and collection title prefixes, names of sales channels to publish collections to, run daily, run hourly, and test mode.

15-day free trial – unlimited tasks

Documentation

Running on a schedule or manually, this task will create automated collections based on the validation values of the configured product metafield definitions. Additionally, configuring one or more exact sales channel names will enable publishing of any newly created collections by this task to those sales channels.

To configure the 'Product metafields and collection title prefixes' field, use the left-hand side to enter metafield definition identifiers in the format of namespace.key (e.g. "custom.product_colors"), and the right-hand side to enter the optional title prefix (e.g. "Color: ") for any collections created for that metafield's validation values.

Upon creation by this task, an automated collection will have a single condition set which auto-includes any products that use the paired metafield definition and value. All other aspects of the collection (e.g. title, handle, sort order, additional conditions, sales channels, etc.) can be manually changed after creation if needed.

To work with this task, product metafields will need to be activated as a collection condition.

Notes:
- This task only supports metafields of type "Single line text" or "Single line text (List)".
- Before creating a new collection, this task will verify that there are no existing collections that use that specific metafield value as a product metafield condition.
- This task will log any collections found that use an outdated value from a metafield definition.

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
{% if options.run_hourly__boolean %}
  mechanic/scheduler/hourly
{% elsif options.run_daily__boolean %}
  mechanic/scheduler/daily
{% endif %}
mechanic/user/trigger 
mechanic/actions/perform
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
product metafields and collection title prefixes (keyval, required), names of sales channels to publish collections to (array), run daily (boolean), run hourly (boolean), test mode (boolean)
Code
{% assign product_metafields_and_collection_title_prefixes = options.product_metafields_and_collection_title_prefixes__keyval_required %}
{% assign sales_channel_names = options.names_of_sales_channels_to_publish_collections_to__array %}
{% assign run_daily = options.run_daily__boolean %}
{% assign run_hourly = options.run_hourly__boolean %}
{% assign test_mode = options.test_mode__boolean %}

{% if event.preview %}
  {% assign product_metafields_and_collection_title_prefixes = hash %}
  {% assign product_metafields_and_collection_title_prefixes["custom.product_colors"] = "Color: " %}
{% endif %}

{% assign product_metafields = product_metafields_and_collection_title_prefixes | keys %}

{% if event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %}
  {% if sales_channel_names != blank %}
    {% comment %}
      -- check if the configured sales channels exist in this shop by name; save the publication IDs for lookup later
    {% endcomment %}

    {% capture query %}
      query {
        publications(
          first: 250
          catalogType:APP
        ) {
          nodes {
            id
            catalog {
              ... on AppCatalog {
                apps(first: 1) {
                  nodes {
                    title
                  }
                }
              }
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "publications": {
              "nodes": [
                {
                  "id": "gid://shopify/Publication/1234567890",
                  "catalog": {
                    "apps": {
                      "nodes": [
                        {
                          "title": {{ sales_channel_names.first | json }}
                        }
                      ]
                    }
                  }
                }
              ]
            }
          }
        }
      {% endcapture %}

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

    {% assign publication_ids = array %}
    {% assign available_sales_channel_names = array %}

    {% for publication in result.data.publications.nodes %}
      {% assign publication_name = publication.catalog.apps.nodes.first.title %}

      {% assign available_sales_channel_names = available_sales_channel_names | push: publication_name %}

      {% if sales_channel_names contains publication_name %}
        {% assign publication_ids = publication_ids | push: publication.id %}
      {% endif %}
    {% endfor %}

    {% unless event.preview %}
      {% if publication_ids == blank  %}
        {% error
          message: "None of the sales channel configured in this task exist in the shop. Check the list of available channels and verify each configured channel exists.",
          configured_sales_channel_names: sales_channel_names,
          available_sales_channel_names: available_sales_channel_names
        %}

        {% break %}

      {% elsif publication_ids.size != sales_channel_names.size %}
        {% comment %}
          -- using action error here so the task will continue with any other configured and matched sales channels
        {% endcomment %}

        {% action "echo"
          __error: "One or more configured sales channel names do not match any of the publication names available in this shop.",
          configured_sales_channel_names: sales_channel_names,
          available_sales_channel_names: available_sales_channel_names
        %}
      {% endif %}
    {% endunless %}
  {% endif %}

  {% comment %}
    -- loop through all configured metafields to get their metafield definition and validation values to be used as collection conditions
  {% endcomment %}

  {% assign metafield_definitions = array %}

  {% for product_metafield in product_metafields %}
    {% capture query %}
      query {
        metafieldDefinitions(
          first: 1
          ownerType: PRODUCT
          namespace: {{ product_metafield | split: "." | first | json }}
          key: {{ product_metafield | split: "." | last | json }}
        ) {
          nodes {
            id
            name
            namespace
            key
            ownerType
            type {
              name
            }
            useAsCollectionCondition
            validations {
              value
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "metafieldDefinitions": {
              "nodes": [
                {
                  "id": "gid://shopify/MetafieldDefinition/1234567890",
                  "name": "Product Colors",
                  "namespace": "custom",
                  "key": "product_colors",
                  "ownerType": "PRODUCT",
                  "type": {
                    "name": "list.single_line_text_field"
                  },
                  "useAsCollectionCondition": true,
                  "validations": [
                    {
                      "value": "[\"Blue\",\"Red\"]"
                    }
                  ]
                }
              ]
            }
          }
        }
      {% endcapture %}

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

    {% assign metafield_definition = result.data.metafieldDefinitions.nodes.first %}
    {% assign metafield_values
      = metafield_definition.validations.first.value
      | default: "[]"
      | parse_json
    %}

    {% comment %}
      -- validate that the metafield definition exists and is configured correctly
      -- using action errors for validation so the task will continue running and check all configured metafields
    {% endcomment %}

    {% if metafield_definition == blank %}
      {% action "echo"
        __error: "Metafield definition not found for this configured metafield and owner type (PRODUCT).",
        product_metafield: product_metafield
      %}
      {% continue %}
    {% endif %}

    {% unless metafield_definition.type.name contains "single_line_text_field" %}
      {% action "echo"
        __error: "Metafield definition for this configured metafield must be a type of 'Single line text' or 'Single line text (List)'.",
        metafield_definition: metafield_definition,
        product_metafield: product_metafield
      %}
      {% continue %}
    {% endunless %}

    {% unless metafield_definition.useAsCollectionCondition %}
      {% action "echo"
        __error: "Metafield definition for this configured metafield must be set for use by 'Automated collections'.",
        metafield_definition: metafield_definition,
        product_metafield: product_metafield
      %}
      {% continue %}
    {% endunless %}

    {% if metafield_values == blank %}
      {% action "echo"
        __error: "Metafield definition validation values not found for this configured metafield.",
        metafield_definition: metafield_definition,
        product_metafield: product_metafield
      %}
      {% continue %}
    {% endif %}

    {% comment %}
      -- all validation checks passed; extend the metafield definition object to store collection data
    {% endcomment %}

    {% assign metafield_definition["metafield_values"] = metafield_values %}
    {% assign metafield_definition["collection_title_prefix"] = product_metafields_and_collection_title_prefixes[product_metafield] %}
    {% assign metafield_definition["collections_by_value"] = hash %}

    {% for metafield_value in metafield_values %}
      {% assign metafield_definition["collections_by_value"][metafield_value] = array %}
    {% endfor %}

    {% assign metafield_definitions = metafield_definitions | push: metafield_definition %}
  {% endfor %}

  {% if metafield_definitions == blank %}
    {% log "None of the configured metafields meet all of the necessary conditions for usage by this task." %}
    {% break %}
  {% endif %}

  {% comment %}
    -- query automated collections; save collection details for any that use a metafield configured in this task as a condition
  {% endcomment %}

  {% assign cursor = nil %}

  {% for n in (1..100) %}
    {% capture query %}
      query {
        collections(
          first: 250
          after: {{ cursor | json }}
          query: "collection_type:smart"
        ) {
          pageInfo {
            hasNextPage
            endCursor
          }
          nodes {
            id
            title
            handle
            templateSuffix
            sortOrder
            ruleSet {
              appliedDisjunctively
              rules {
                column
                relation
                condition
                conditionObject {
                  __typename
                  ... on CollectionRuleMetafieldCondition {
                    metafieldDefinition {
                      id
                    }
                  }
                }
              }
            }
          }
        }

      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "collections": {
              "nodes": [
                {
                  "id": "gid://shopify/Collection/1234567890",
                  "ruleSet": {
                    "rules": [
                      {
                        "column": "PRODUCT_METAFIELD_DEFINITION",
                        "relation": "EQUALS",
                        "condition": "Blue",
                        "conditionObject": {
                          "metafieldDefinition": {
                            "id": "gid://shopify/MetafieldDefinition/1234567890"
                          }
                        }
                      }
                    ]
                  }
                },
                {
                  "id": "gid://shopify/Collection/2345678901",
                  "ruleSet": {
                    "rules": [
                      {
                        "column": "PRODUCT_METAFIELD_DEFINITION",
                        "relation": "EQUALS",
                        "condition": "Grey",
                        "conditionObject": {
                          "metafieldDefinition": {
                            "id": "gid://shopify/MetafieldDefinition/1234567890"
                          }
                        }
                      }
                    ]
                  }
                }
              ]
            }
          }
        }
      {% endcapture %}

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

    {% for collection in result.data.collections.nodes %}
      {% if collection.ruleSet == blank %}
        {% continue %}
      {% endif %}

      {% for rule in collection.ruleSet.rules %}
        {% unless rule.column == "PRODUCT_METAFIELD_DEFINITION" and rule.relation == "EQUALS" %}
          {% continue %}
        {% endunless %}

        {% assign matched_metafield_definition
          = metafield_definitions
          | where: "id", rule.conditionObject.metafieldDefinition.id
          | first
        %}

        {% unless matched_metafield_definition %}
          {% continue %}
        {% endunless %}

        {% unless matched_metafield_definition.metafield_values contains rule.condition %}
          {% unless event.preview %}
            {% log
              message: "Found collection using outdated metafield value condition for the matched metafield definition.",
              collection: collection,
              matched_metafield_definition: matched_metafield_definition
            %}
          {% endunless %}

          {% continue %}
        {% endunless %}

        {% assign matched_metafield_definition["collections_by_value"][rule.condition]
          = matched_metafield_definition["collections_by_value"][rule.condition]
          | push: collection.id
        %}
      {% endfor %}
    {% endfor %}

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

  {% unless event.preview %}
    {% log metafield_definitions_with_extended_data: metafield_definitions %}
  {% endunless %}

  {% comment %}
    -- check all extended metafield definition objects to see if any new collections need to be made on this task run
  {% endcomment %}

  {% assign collections_to_create = array %}

  {% for metafield_definition in metafield_definitions %}
    {% for metafield_value in metafield_definition.metafield_values %}
      {% if metafield_definition.collections_by_value[metafield_value] == blank %}
        {% assign product_metafield = metafield_definition.namespace | append: "." | append: metafield_definition.key %}

        {% assign collection_to_create = hash %}
        {% assign collection_to_create["product_metafield"] = product_metafield %}
        {% assign collection_to_create["collection_title"] = metafield_value | prepend: metafield_definition.collection_title_prefix %}
        {% assign collection_to_create["metafield_value"] = metafield_value %}
        {% assign collection_to_create["metafield_definition_id"] = metafield_definition.id %}
        {% assign collections_to_create = collections_to_create | push: collection_to_create %}
      {% endif %}
    {% endfor %}
  {% endfor %}

  {% unless event.preview %}
    {% log collections_to_create: collections_to_create %}
  {% endunless %}

  {% if test_mode %}
    {% log "This task is set for 'Test mode' and no new collections will be created." %}
    {% break %}
  {% endif %}

  {% for collection_to_create in collections_to_create %}
    {% capture mutation %}
      mutation {
        collectionCreate(
          input: {
            title: {{ collection_to_create.collection_title | json }}
            ruleSet: {
              appliedDisjunctively: false
              rules: [
                {
                  column: PRODUCT_METAFIELD_DEFINITION
                  relation: EQUALS
                  condition: {{ collection_to_create.metafield_value | json }}
                  conditionObjectId: {{ collection_to_create.metafield_definition_id | json }}
                }
              ]
            }
          }
        ) {
          collection {
            id
            title
            handle
          }
          userErrors {
            field
            message
          }
        }
      }
    {% endcapture %}

    {% action %}
      {
        "type": "shopify",
        "options": {{ mutation | json }},
        "meta": {
          "publication_ids": {{ publication_ids | json }}
        }
      }
    {% endaction %}
  {% endfor %}

{% elsif event.topic == "mechanic/actions/perform" %}
  {% comment %}
    -- only respond to successful creation of collections
  {% endcomment %}

  {% unless action.type == "shopify" and action.run.ok and action.run.result.data.collectionCreate %}
    {% break %}
  {% endunless %}

  {% assign collection_id = action.run.result.data.collectionCreate.collection.id %}
  {% assign publication_ids = action.meta.publication_ids %}

  {% comment %}
    -- publish the new collection to all of the valid publications configured in the task
  {% endcomment %}

  {% assign mutations = array %}

  {% for publication_id in publication_ids %}
    {% capture mutation %}
      publishablePublish{{ forloop.index }}: publishablePublish(
        id: {{ collection_id | json }}
        input: {
          publicationId: {{ publication_id | json }}
        }
      ) {
        publishable {
          ... on Collection {
            id
            title
            handle
          }
        }
        userErrors {
          field
          message
        }
      }
    {% endcapture %}

    {% assign mutations = mutations | push: mutation %}
  {% endfor %}

  {% if mutations != blank %}
    {% action "shopify" %}
      mutation {
        {{ mutations | join: newline }}
      }
    {% endaction %}
  {% endif %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Product metafields and collection title prefixes
{"custom.product_color" => "Color: "}
Names of sales channels to publish collections to
["Online Store"]
Test mode
true