How to access app owned meta fields with namespace $app:whatever in .liquid files

Topic summary

A developer is struggling to access app-owned metafields with the $app: namespace prefix in Shopify Liquid files.

The Problem:

  • They can successfully create and read metafields in their admin TSX files using the $app:discount-shipping namespace
  • However, accessing these same metafields in .liquid files using shop.metafields['$app:discount-shipping']['function-configuration'].value doesn’t work
  • Hardcoded namespaces (without $app: prefix) work fine in Liquid files

Attempted Solutions:

  • Created proper metafield definitions with PUBLIC_READ storefront access
  • Tried both | parse_json and | json filters (CLI gives errors with | json)
  • Used the standard Liquid syntax for accessing metafields

Current Status:
The issue remains unresolved. One respondent suggested ensuring the metafield’s storefront access is set to PUBLIC_READ and provided syntax examples, but the original poster confirmed this didn’t resolve the problem. The developer suspects the issue may be related to how they’re saving the metafields through the GraphQL metafieldsSet mutation.

The discussion also includes a side rant about Shopify’s development experience, questioning why Liquid templating exists when JSX/TSX is already used in the admin panel.

Summarized with AI on November 4. AI used: claude-sonnet-4-5-20250929.

Hello… Shopify… development…

I’m kinda hacking my way through developing custom shopify apps, i have met satan a few times throughout my 2-3 weeks of development (no native cart add/remove events for one, which ultimately would be a 20 minutes addon for any experienced developer… a simple custom js event, so that you don’t have to query any API to get changes.).

How do I access app-owned metafields in Shopify .liquidated files?

This is how I hoped I could do it:

shop.metafields['$app:discount-shipping']['function-configuration']

This is how I set/read the metafields in my admin .tsx file:

export const loader: LoaderFunction = async ({request}) => {
    const {admin} = await authenticate.admin(request);
    const metaFields = await getShopMetafields(admin, "$app:discount-shipping", 10);

    let settings = {};
    if (metaFields && metaFields[0]) {
        settings = metaFields[0];
    }

    return {
        settings: settings,
    }
};

export async function action({request}: {request: Request}) {
    const formData = await request.formData();
    const {admin} = await authenticate.admin(request);
    const shop = await getShop(admin);

    const responseMetafieldSet = await setMetaField(
        admin,
        "$app:discount-shipping",
        "function-configuration",
        "json",
        JSON.stringify({
            targetSetting: formData.get("targetSetting"),
            discountValue: Number(formData.get("discountValue")),
            discountType: formData.get("discountType"),
            textAlignment: formData.get("textAlignment"),
            completedBackground: formData.get("completedBackground"),
            missingBackground: formData.get("missingBackground"),
        }),
        shop.id
    );

    const errors = responseMetafieldSet?.userErrors;
    if (errors && errors.length > 0) {
        return json({ success: false, errors: errors});
    }

    return json({ success: true});
}

And the helper function:

export async function getShopMetafields(
    admin: AdminApiContext<RestResources>,
    namespace: string,
    first: number = 10
) {
    return await admin.graphql(`#graphql
    query {
        shop {
            metafields(namespace: "${namespace}", first: ${first}) {
                edges {
                    node {
                        id
                        namespace
                        key
                        value
                        type
                    }
                }
            }
        }
    }`
    ).then((e: any) => e.json())
    .then((e: any) => e.data.shop.metafields.edges.map((edge: any) => edge.node));
}

How do I access the app owned meta field? right now I am doing this, which works if I ofcourse change the namespace in my read/write functions above:

{% assign free_shipping_settings_json = shop.metafields.checkout_champ_free_shipping.settings | parse_json %}

Also, “| parse_json” works… but gives CLI errors “| json” does not work.

Random Rant:
Why have shopify even created a .liquid template engine, basically we are using JSX/TSX to develop components in the admin panel, why not use the same instead of .liquid files, then we could reuse components? to me this whole .liquid templating seems utterly bleep, I have to build stuff twice… once for a visual representation in the admin panel, and a completely other one in the .liquid files.

@Webwizzy

Hi there,

Accessing app-owned metafields in Shopify’s .liquid files can indeed be a bit tricky. To retrieve the metafields you’ve set in your app, you can use the following syntax:

{% assign metafield_value = shop.metafields['$app:your_namespace']['your_key'].value %}

For example, if you have defined a namespace like $app:discount-shipping, you would access it as follows:

{% assign free_shipping_settings_json = shop.metafields['$app:discount-shipping']['function-configuration'].value %}

This works because it’s being used in my theme app extension and not in any .liquid file within the theme. You should also find that it works without the need to add | parse_json if the metafield is already in JSON format. You can find more details in the Shopify documentation here.

When creating the metafield, ensure that you set the access’s storefront field to PUBLIC_READ (as explained here). Here’s an example template for creating the given metafield using your values:

{
 "definition": {
  "name": "Discount Shipping Settings",
  "namespace": "$app:discount-shipping",
  "key": "function-configuration",
  "type": "json",
  "description": "Configuration settings for discount shipping.",
  "ownerType": "SHOP",
  "access": {
   "storefront": "PUBLIC_READ"
   ...
  }
 },
 "value": {
  ...
 }
}

Additionally, if you’re using this outside of theme extensions, you might want to explicitly set the namespace as app–<app_id> instead of $app.

If you have any more questions, feel free to reach out!

Cheers,
Foladun | Deluxe: Account & Loyalty

2 Likes

Hello Deluxe, thanks for taking the time to reply to my message.

Unfortunately, your solution didn’t make any changes, I’m still not able to load theapp-owned namespace, I am however able to load the hardcoded namespace in my .liquid files :disappointed_face:

I am not using them outside the theme extension, basically, I just create a .liquid file that I can use in the drag/drop builder and the meta field is a shop meta field created from this method:

export async function setMetaField(
    admin: AdminApiContext<RestResources>,
    namespace: string,
    key: string,
    type: string,
    value: any,
    ownerId: any,
): Promise<{
    metafield?: {
        id: string,
        namespace: string,
        key: string,
        value: string,
    },
    userErrors?: {
        field: string[],
        message: string,
        code: string,
    }[]
}> {
    // Send the mutation to create or update the metafield
    const response = await admin.graphql(`
            mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
                metafieldsSet(metafields: $metafields) {
                    metafields {
                        id
                        namespace
                        key
                        value
                    }
                    userErrors {
                        field
                        message
                        code
                    }
                }
            }
        `,
        {
            variables: {
                metafields: [
                    {
                        namespace: namespace,
                        key: key,
                        type: type,
                        value: value,
                        ownerId: ownerId
                    }
                ]
            }
        }
    );

    const jsonResponse = await response.json();
    const result = jsonResponse.data.metafieldsSet;

    if (result.userErrors && result.userErrors.length > 0) {
        return { userErrors: result.userErrors };
    }

    return { metafield: result.metafields[0] };
}

Is it because of the way I save the metafields?

I have created the definition:

export const loader: LoaderFunction = async ({request}) => {
    const {admin} = await authenticate.admin(request);

    const fieldDefinition = await setMetaFieldDefinition(
        admin,
        'Discount - Shipping',
        '$app:discount-shipping',
        'function-configuration',
        'json',
        'SHOP',
        {
            admin: 'MERCHANT_READ_WRITE',
            storefront: 'PUBLIC_READ',
        }
    )

And this is the helper method that created the definition:

export async function setMetaFieldDefinition(
    admin: AdminApiContext<RestResources>,
    name: string,
    namespace: string,
    key: string,
    metafieldType: metafieldTypes,
    ownerType: ownerType,
    accessTypes: accessTypes,

): Promise<{
    metafield?: {
        id: string,
        namespace: string,
        key: string,
        value: string,
    },
    userErrors?: {
        field: string[],
        message: string,
        code: string,
    }[]
}> {
    // Send the mutation to create or update the metafield
    const response = await admin.graphql(`
        mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
            metafieldDefinitionCreate(definition: $definition) {
                createdDefinition {
                    id
                    namespace
                    key
                    access {
                        admin
                        storefront
                    }
                }
                userErrors {
                    field
                    message
                    code
                }
            }
        }
    `,
        {
            variables: {
                definition: {
                    name: name,
                    namespace: namespace,
                    key: key,
                    type: metafieldType,
                    ownerType: ownerType,
                    access: accessTypes
                }
            }
        }
    );

    const jsonResponse = await response.json();
    const result = jsonResponse.data.metafieldDefinitionCreate;

    if (result.userErrors && result.userErrors.length > 0) {
        return { userErrors: result.userErrors };
    }

    return {metafield: result.createdDefinition};
}

This is how I try to access it in the .liquid file:

{% assign free_shipping_settings_json = shop.metafields['$app:discount-shipping']['function-configuration'].value %}

But it is not working like before, when I saved it as hardcoded namespace and key :disappointed_face: