The 2026-04 changelog was dominated by deadlines — the Scripts shutdown, inventory API rewrites, expiring access tokens. But buried under the breaking changes is the most useful thing Shopify shipped for app developers this year: every Function can now read metaobjects.
It sounds like a footnote. It isn’t. It changes where your business logic is allowed to live — and it quietly removes one of the most painful constraints of building on Functions.
A quick refresher on metaobjects
A metaobject is a custom, structured data record. You define a definition (the shape — a set of typed fields like “min subtotal”, “percentage”, “active”), and merchants or apps create entries (the actual data) against it. Think of the definition as a database table and each entry as a row.
There are two flavors that matter here: app-owned metaobjects (created and namespaced by your app, prefixed $app:) and merchant-owned metaobjects (created by the merchant in Settings → Custom data). The distinction drives the access rules below.
What actually changed in 2026-04
Three things, and together they’re more than the sum of their parts:
- Every function target can read metaobjects. Not just discounts — delivery customization, cart transform, validation, payment customization, fulfillment constraints. All of them.
- You query entries by handle or ID directly in the function’s input query, or by traversing a metafield reference.
- App-owned metaobjects need no access scopes on Admin API 2026-04+. Merchant-owned metaobjects still require the relevant read scope.
Why it matters
Before this, dynamic config had to be baked into the function or squeezed into a single metafield blob. Now your logic reads structured, merchant-editable data at runtime. A merchant changes a campaign by editing a record — you don’t redeploy the function.
The old way, and why it hurt
Functions run as sandboxed WebAssembly with no general network access (the fetch target is restricted to custom apps on Plus and Enterprise). So if a discount needed to change — new threshold, new percentage, a seasonal message — your options were grim: hardcode it and redeploy, or stuff everything into one JSON metafield and hand-roll a schema with no validation and no decent admin UI.
Metaobjects fix both problems at once. They’re structured (typed fields, validation, a real admin editing surface) and readable from inside the function. Configuration finally has a proper home.
Pattern A: traverse a metafield reference
The most robust pattern. Your discount carries a metafield that references a metaobject entry, and the input query follows that reference. Here a campaign record drives the whole discount:
src/cart_lines_discounts_generate_run.graphql
query Input {
cart {
cost { subtotalAmount { amount } }
}
discount {
discountClasses
metafield(namespace: "$app:config", key: "active_campaign") {
reference {
... on Metaobject {
active: field(key: "active") { value }
minSubtotal: field(key: "min_subtotal") { value }
percentage: field(key: "percentage") { value }
message: field(key: "message") { value }
}
}
}
}
}
The run function reads the entry and applies the discount — no thresholds or copy baked into the code:
src/cart_lines_discounts_generate_run.js
import { OrderDiscountSelectionStrategy } from "../generated/api";
export function cartLinesDiscountsGenerateRun(input) {
const campaign = input.discount.metafield?.reference;
if (!campaign) return { operations: [] };
const active = campaign.active?.value === "true";
const min = parseFloat(campaign.minSubtotal?.value ?? "0");
const pct = parseFloat(campaign.percentage?.value ?? "0");
const message = campaign.message?.value ?? "Discount";
const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
if (!active || pct <= 0 || subtotal < min) return { operations: [] };
return {
operations: [{
orderDiscountsAdd: {
selectionStrategy: OrderDiscountSelectionStrategy.First,
candidates: [{
message,
targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
value: { percentage: { value: pct } },
}],
},
}],
};
}
Now the merchant opens the campaign entry, flips active to true, sets min_subtotal to 75 and percentage to 12, and the live discount updates. Your code never moves.
Pattern B: look up an entry by handle
New in 2026-04, you can also pull an entry directly by type and handle, without a metafield acting as the link. Handles are only unique within a type, so you pass both:
direct lookup in the input query
query Input {
cart { cost { subtotalAmount { amount } } }
campaign: metaobject(
handle: { type: "$app:promo_campaign", handle: "current" }
) {
fields { key value }
}
}
This is handy for singletons — a “current promotion” or a global settings record — where you don’t want to attach a reference to every discount. Field availability depends on your target’s input schema, so confirm it against the GraphiQL explorer for your specific function before shipping.
Beyond discounts
Because every target gained this, the same merchant-managed-config pattern now applies across the platform:
- Delivery customization — hide, rename, or reorder shipping options based on rules stored in a metaobject (region tiers, weight bands, cutoff times).
- Cart & checkout validation — block or warn on carts using merchant-editable rule sets instead of redeployed conditionals.
- Cart transform — drive bundle definitions and component pricing from structured bundle records.
- Payment customization — hide or reorder payment methods by rules a merchant can actually edit.
- Fulfillment constraints — express location and grouping logic as data, not code.
The throughline: logic that used to require an app redeploy becomes a record a merchant edits in the admin.
Gotchas worth knowing
- Set
api_version = "2026-04"(or later) in your extension TOML — this is gated on the function’s API version. - App-owned vs merchant-owned. App-owned (
$app:) entries read with no scopes; merchant-owned entries still need the relevant read scope. Decide ownership deliberately — app-owned is cleaner for config you control, merchant-owned for data the merchant truly owns. - Query only the fields you use. Function input size affects execution budget; don’t pull large entry lists into a checkout-path function.
- Handle uniqueness is per type. Always pass the type when looking up by handle.
- This replaces most
fetchhacks. Public apps that previously couldn’t call out mid-checkout can now ship genuinely configurable logic with zero network dependency — faster and far more reliable.
The takeaway
Metaobject access turns Shopify Functions from “logic you deploy” into “logic that reads merchant data.” For app developers that means fewer redeploys, real configuration with validation and a proper admin UI, and the same pattern reused across discounts, shipping, validation, and more. If you maintain a Functions-based app, this is the upgrade to plan for in your next iteration — move your hardcoded config into metaobjects and let merchants drive it.



