Metafields and Metaobjects within an unlisted Shopify App

I recently created a custom app. Along side some other things, this app creates a metafield using the metafieldDefinitionCreate GraphQL mutation which is a mixed reference using four metaobjects the app also creates (within the shopify.app.toml). This all works after installing the app however if I update the app or reinstall it, I run into so strangeness I can’t explain:

  1. The metaobject definitions created by the app shows the correct number of entries when looking at the list of metaobject definitions but when you click on one and view the entries, there are no entries to show.
  2. When looking at the metafield, I can see it is a mixed reference linked to 4 metaobjects (which is correct) but when I click on the list of metaobjects for that metafield, there are non-selected. I can see the app-created metaobjects but they are not selected. I can select them as if they are an additional 4 I want to included (in addition to the 4 ghost ones it thinks are there)

When all of this happens, I can manually re-assign my 4 metaobjects to that metafiled and add new metaobject entries to make everything work again but the fact that it seems to completely loose track of the metaobject relationships is really strange.

I have confirmed that when this all happens, the metafield and metaobject definition handles are not changing so that’s not the issue. When I view the /content/metaobjects/entries, the missing metaobject entries are not there either so even if it were simply not associating the old entries to the new metaobject definitions, I would have expected to see them there - I don’t.

This feels like a bug in Shopify that needs to be addressed. I think I’m doing everything right but I’ll be totally honest, this is the first time I’ve tried doing anything like this so it’s possible I’m doing something wrong. Open to feedback or suggestions on what I can do to fix this issue.

Hey @Jeff_Nelon - thanks for flagging this.

This does sound like it could be an issue on our end with how app-owned metaobject definitions are being tracked after an app update or reinstall. I can’t confirm that fully from the thread alone at the moment, but the behaviour you’re seeing definitely looks odd: the Admin appears to know there are related definitions/entries, but then the entries and selected reference targets don’t line up when you drill into them.

One thing I’d want to verify is whether the mixed-reference metafield definition is still pointing at older internal metaobject definition IDs after the app-owned definitions are recreated. Even if the handles stay the same, the underlying definition references may not be the same after reinstall, which could explain the “ghost” selections you’re seeing.

Could you share a bit more detail here?

  • The relevant sanitized shopify.app.toml sections for those metaobject definitions
  • The metafieldDefinitionCreate input you’re using for the mixed reference metafield and an X-Request-ID from the response header for the creation API call
  • The Admin API version you’re using
  • Confirming whether this happens only after a full uninstall/reinstall, or also after running an app deploy/update without uninstalling?

For prevention, it may be worth defining the metafield in shopify.app.toml as well if possible, using the $app: mixed-reference syntax, but I’d like to confirm the current setup before saying that’s definitely the right path.

I’ll look into this further on our end here too, since this may need cleanup or investigation from Shopify’s side if those entries/references are getting orphaned. Hope to hear from you soon.

@Alan_G

API Version is 2025-01. This appears to be an issue only when Uninstalling and re-installing my app I believe. I do run into it a bit when running tests on pre-deployed versions using the Shopify CLI but that is a little intermittent so not sure if that is just a side-effect of using the dev environment.

As far as creating the metafields in the shopify.app.toml, that would be ideal however in testing, it appears that toml created metafields cannot be pinned which, for my application is necessary. Unless you’re able to provide an example of toml code that creates a metafield that can be pinned. That would be great.

Here is a snippet from the shopify.app.toml

[metaobjects.app.rackgrp_builder_options]
name = "Product Builder Option"
description = "Choices customers can make to customer/build their product."
display_name_field = "admin_label"

  [metaobjects.app.rackgrp_builder_options.access]
  admin = "merchant_read_write"
  storefront = "public_read"

  [metaobjects.app.rackgrp_builder_options.capabilities]
  publishable = true

[metaobjects.app.rackgrp_builder_options.fields.admin_label]
name = "Display Name"
type = "single_line_text_field"
description = "For organizational purposes. Not visible to the public."

[metaobjects.app.rackgrp_builder_options.fields.label]
name = "Label"
type = "single_line_text_field"
description = "Label/Heading shown to the public."

[metaobjects.app.rackgrp_builder_options.fields.template]
name = "Template"
type = "single_line_text_field"
description = "How the options are displayed on the website."

  [metaobjects.app.rackgrp_builder_options.fields.template.validations]
  choices = [
    "List",
    "Small Thumbnails",
    "Large Thumbnails"
  ]

[metaobjects.app.rackgrp_builder_options.fields.options]
name = "Options"
type = "list.single_line_text_field"
description = "Non-inventory options (eg. 'None' or 'Diameter')."

[metaobjects.app.rackgrp_builder_options.fields.products]
name = "Products"
type = "list.product_reference"
description = "Products the customer can choose from for this option."

[metaobjects.app.rackgrp_builder_options.fields.hide_price]
name = "Hide Price/Included"
type = "boolean"
description = "Do not show individual pricing or 'Included' for products in this group."

[metaobjects.app.rackgrp_builder_options.fields.default_variant_behavior]
name = "Default Variant Behavior"
type = "single_line_text_field"
description = "How the builder treats default and non-default choices."

  [metaobjects.app.rackgrp_builder_options.fields.default_variant_behavior.validations]
  choices = [
    "Included In price",
    "Addition to price",
    "Excluded from order"
  ]

[metaobjects.app.rackgrp_builder_options.fields.included_variants]
name = "Included Variants"
type = "list.variant_reference"
description = "Products that are automatically added to the order along with chosen product options."

[metaobjects.app.rackgrp_builder_options.fields.z_index]
name = "Z-index"
type = "number_integer"
description = "Used to layer preview images. Higher number will be displayed over a lower number."

…and here is a snippet from my app JS that creates the metafields. You’ll notice that I am hard-coding the metafield targetHandles to match the ID’s in my app but I have tried not doing that and I still have the same issues.

let { ownerType, namespace, key, name, type, description, isDynamic } = def;

try {
	const checkQuery = `
		query {
			metafieldDefinitions(first: 1, ownerType: ${ownerType}, namespace: "${namespace}", key: "${key}") {
				edges { node { id name description } }
			}
		}
	`;
	const checkResJson = await shopifyGraphqlFetch(shop, accessToken, checkQuery);
	const existing = checkResJson?.data?.metafieldDefinitions?.edges[0]?.node;
	console.log(`[Metafields] Checking '${namespace}.${key}': ${existing ? "FOUND (ID: " + existing.id + ")" : "NOT FOUND"}`);

	if (existing) {
		if (existing.name !== name || existing.description !== description) {
			console.log(`[Metafields] Updating '${namespace}.${key}'...`);
			const updateMutation = `
				mutation {
					metafieldDefinitionUpdate(id: "${existing.id}", definition: { name: "${name}", description: "${description}" }) {
						userErrors { message }
					}
				}
			`;
			await shopifyGraphqlFetch(shop, accessToken, updateMutation);
		}
	} else {
		console.log(`[Metafields] Creating '${namespace}.${key}'...`);
		let validations = "";
		if (isDynamic) {
			const targetHandles = ["app--290631450625--rackgrp_builder_template", "app--290631450625--rackgrp_builder_options", "app--290631450625--rackgrp_builder_field", "app--290631450625--rackgrp_builder_content"];
			const gids = targetHandles.map(h => gidMap[h]).filter(Boolean);
			validations = `, validations: [{name: "metaobject_definition_ids", value: "${JSON.stringify(gids).replace(/"/g, '\\"')}"}]`;
		}

		const createMutation = `
			mutation {
				metafieldDefinitionCreate(definition: { 
					name: "${name}", namespace: "${namespace}", key: "${key}", 
					ownerType: ${ownerType}, type: "${type}", description: "${description}" ${validations} 
				}) {
					userErrors { message }
				}
			}
		`;
		const createResJson = await shopifyGraphqlFetch(shop, accessToken, createMutation);
		const errors = createResJson?.data?.metafieldDefinitionCreate?.userErrors;
		if (errors && errors.length > 0) {
			console.error(`[Metafields] Creation error for '${namespace}.${key}':`, errors);
			allDefinitionsSuccessful = false;
		}
	}
} catch (error) {
	console.error(`[Metafields] Reconciliation error on ${namespace}.${key}:`, error.message);
	allDefinitionsSuccessful = false;
}

Hey @Jeff_Nelon - thanks for sending that over, this is really helpful.

You’re right on the TOML piece here: I don’t have a shopify.app.toml example that creates a pinned metafield definition, because declarative/app-owned metafield definitions don’t currently support pinning. If the definition needs to be pinned, creating/managing that metafield definition through the Admin API is the right path, so you’re on point there.

The main thing I would suggest taking a look at in your reconciliation code is that the existing-definition branch should also refresh the mixed-reference validation, not just the name/description.

Right now, after an uninstall/reinstall, your app can recreate the app-owned metaobject definitions with the same handles/types, but the underlying definition IDs may be different. If the existing metafield definition keeps the old metaobject_definition_ids, that would explain the “ghost” selected definitions you’re seeing.

So I’d suggest either:

  1. Re-query the current metaobject definitions after install/reinstall and update the metafield definition’s validations with the fresh IDs; or
    1. If it works for your setup, use metaobject_definition_types instead of metaobject_definition_ids for the mixed-reference validation, so the definition is less tightly coupled to recreated GIDs.

For example, your “existing definition” path should do something like a metafieldDefinitionUpdate with the same ownerType, namespace, and key, plus the current validations:

mutation UpdateDefinition($definition: MetafieldDefinitionUpdateInput!) {
  metafieldDefinitionUpdate(definition: $definition) {
    updatedDefinition {
      id
      validations {
        name
        value
      }
      pinnedPosition
    }
    userErrors {
      field
      message
      code
    }
  }
}

One other note: 2025-01 is outside the currently supported Admin API version window, so I’d also check the X-Shopify-API-Version response header to confirm which version Shopify is actually serving for those requests. We do automatically bump you to the oldest support API version which is 2025-07 currently, but just wanted to mention this as something to check too.

The missing metaobject entries also sound like they may overlap with a known issue with orphaned app-owned metaobject behavior after uninstall/reinstall. If the entries are already stuck/orphaned on the store, just let me know and I can take a closer look on our end here.

If you’re able to test the validation refresh and still see the ghost references, could you share an X-Request-ID from the metafieldDefinitionCreate or metafieldDefinitionUpdate call, plus the current validation value being returned for that metafield definition? Hope this helps!