For years, every business rule you wanted inside a Shopify Function had to be baked into the WebAssembly binary or smuggled in through a single JSON metafield. As of the 2026-04 API version, that constraint is gone: Shopify Functions metaobjects are now readable directly inside your input query. You can pull structured, merchant-editable data — pricing tiers, market rules, eligibility tables — into your discount and validation logic without redeploying a thing. This guide shows developers how the change works, the exact input query syntax, a concrete tiered-discount example, and the pitfalls worth knowing before you ship.
If you have never touched Functions before, start with our Shopify Functions API for beginners and the hands-on build your first custom discount walkthroughs. This piece assumes you already know the basics and want to combine two primitives into something more powerful.
What changed in 2026: Functions can read metaobjects
On April 1, 2026, Shopify shipped metaobject access inside Functions as part of the GraphQL API version 2026-04. Before this, a Function's input query could read the function owner's metafields and the cart, but it could not traverse into a metaobject entry. That meant any structured configuration — say, five discount tiers each with a threshold and a rate — had to be serialized into one metafield value and parsed by hand inside the binary.
Now you can query an app-owned metaobject entry as part of the input query, and Shopify resolves the fields you ask for and hands them to your run target as typed input. The shift is small in code but large in architecture: your Function becomes a thin rules engine, and the rules live in data the merchant can edit.
Why this matters for developers
Three things change in practice:
- No redeploys for rule changes. Update a metaobject entry in the admin and the next checkout uses the new values. The binary never moves.
- Structured beats stringly-typed. Instead of
JSON.parse-ing a blob and hoping the shape holds, you get distinct, validated fields. - Merchants own the data. Operations staff can manage entries themselves, which is exactly what metaobjects were designed for.
If metaobjects are new to you, skim the Shopify metafields guide first — metaobjects are the next step up from a single metafield into a full structured record.
A quick refresher: Functions and Metaobjects
Two primitives are in play here, so a one-paragraph recap of each keeps everyone aligned before the code.
Shopify Functions are small WebAssembly modules that run inside Shopify's backend at well-defined extension points called targets — discounts, cart transforms, delivery customization, validation, and more. A Function has three parts: a configuration file (shopify.extension.toml), an input query (.graphql) that declares the data the Function needs, and the logic (.rs or .js) that receives that input and returns operations. Functions can be written in any language that compiles to Wasm, though Shopify recommends Rust for performance.
Metaobjects are structured, reusable records — like a lightweight content type. Each metaobject definition declares typed fields (single-line text, number, reference, and so on), and each entry is one instance with a unique handle. Unlike a metafield, a metaobject is not attached to a specific product or order; it stands on its own and can be referenced from anywhere.
The "app-owned" requirement
This is the single most important rule to internalize: only app-owned metaobject types are accessible to Functions. App-owned types use the reserved $app: prefix in their type (for example, $app:discount_tiers). A standard merchant-created metaobject type — one without that prefix — will not resolve inside a Function input query. We will come back to why this trips people up in the pitfalls section.
Why combine Shopify Functions metaobjects instead of metafields

You can already feed configuration into a Function through a JSON metafield on the function owner, so why reach for metaobjects? The answer is about structure, scale, and who maintains the data.
| Concern | JSON metafield | App-owned metaobject |
|---|---|---|
| Data shape | One opaque string you parse yourself | Typed fields resolved by Shopify |
| Editing UX | Raw JSON in the admin | Form fields per definition |
| Reuse across functions | Copy the blob each place | One entry, referenced anywhere |
| Validation | Manual, in your binary | Enforced by field types |
| Relationships | Flatten or duplicate | Native references between entries |
A single discount with one threshold fits fine in a metafield. But the moment you have multiple related records — a tier table, per-market rules, a catalog of bundle definitions — metaobjects win because each record is a real entry with its own fields and handle, and the merchant edits it through a proper form instead of hand-writing JSON.
Good fits for the pattern
- Market-aware discounts — one metaobject entry per market with its own rate and minimum.
- Tiered or volume pricing — a table of
{ threshold, rate }rows. - Rule-based validation — allow/deny lists, quantity caps, or B2B eligibility flags.
- Bundle definitions — component products and bundle pricing held as structured entries.
For deeper context on where custom app development is heading, see our overview of the 2026 Shopify custom app changes.
How metaobject access works inside a Function
The mechanics live entirely in the input query. Functions reach metaobjects through the Shop field exposed in the 2026-04 schema, and you target a specific entry by handle or ID.
Budgeting for complexity
Function input queries have a strict complexity budget of 30 points, and metaobject access is metered:
- Each metaobject root costs 1 point.
- Each
field(key:)call costs 3 points.
So a query that reads one metaobject and pulls three fields costs 1 + (3 × 3) = 10 points — comfortably inside budget, but you can see how a wide entry or several metaobjects adds up fast. Ask only for the fields the logic uses.
The shape of the data
When you query a metaobject's fields, each field comes back as a key/value pair where value is a string. Number and boolean fields arrive as their string representation, so your logic parses them. Reference fields return the referenced resource's ID, which you can resolve in the same query if you need its attributes.
Building a tiered discount that reads from a metaobject

Let's make this concrete with a volume discount whose tiers live in an app-owned metaobject. The merchant edits the tiers in the admin; the Function reads them at checkout. We will use the Discount API target cart.lines.discounts.generate.run from the current Discount Function structure.
Step 1: Define the app-owned metaobject
Create a metaobject definition with the type $app:discount_tiers. Give it a JSON or structured field per row, or a single JSON field holding the tier array — for clarity here, assume one entry with three fields: tier_threshold, tier_rate, and message. In a real catalog you would model each tier as its own entry and reference them, but a single configuration entry keeps the example readable.
Step 2: Write the input query
The input query fetches the cart subtotal and reads the tier configuration from the metaobject by handle. Note the $app: type prefix and the field(key:) calls that drive complexity cost.
query Input {
cart {
cost {
subtotalAmount {
amount
}
}
lines {
id
cost {
subtotalAmount {
amount
}
}
}
}
discount {
discountClasses
}
shop {
metaobject(handle: { type: "$app:discount_tiers", handle: "default" }) {
threshold: field(key: "tier_threshold") {
value
}
rate: field(key: "tier_rate") {
value
}
label: field(key: "message") {
value
}
}
}
}This costs roughly 10 complexity points for the metaobject portion (1 root + 3 fields × 3), well under the 30-point budget.
Step 3: Write the run logic
The run target receives the resolved input and returns discount operations. Here is the logic outline in JavaScript — read the metaobject values, parse them, and emit an orderDiscountsAdd operation when the cart clears the threshold.
// src/cart_lines_discounts_generate_run.js
export function cartLinesDiscountsGenerateRun(input) {
const config = input.shop?.metaobject;
const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
// No config entry → no discount, fail safe.
if (!config) {
return { operations: [] };
}
const threshold = parseFloat(config.threshold?.value ?? "0");
const rate = parseFloat(config.rate?.value ?? "0");
const message = config.label?.value ?? "VOLUME DISCOUNT";
if (subtotal < threshold || rate <= 0) {
return { operations: [] };
}
return {
operations: [
{
orderDiscountsAdd: {
selectionStrategy: "FIRST",
candidates: [
{
message,
targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
value: { percentage: { value: rate } },
},
],
},
},
],
};
}The same logic in Rust follows the generated input types from your schema. The structure mirrors the JavaScript: read the metaobject fields off input.shop.metaobject, parse the strings, and return an operation.
// src/main.rs (outline)
#[shopify_function_target(query_path = "src/input.graphql", schema_path = "schema.graphql")]
fn cart_lines_discounts_generate_run(input: input::ResponseData) -> Result<output::CartLinesDiscountsGenerateRunResult> {
let subtotal: f64 = input.cart.cost.subtotal_amount.amount.parse()?;
// Pull the app-owned metaobject config; bail out cleanly if absent.
let config = match input.shop.metaobject {
Some(m) => m,
None => return Ok(no_discount()),
};
let threshold: f64 = field_value(&config.threshold).parse().unwrap_or(0.0);
let rate: f64 = field_value(&config.rate).parse().unwrap_or(0.0);
if subtotal < threshold || rate <= 0.0 {
return Ok(no_discount());
}
// ...build OrderDiscountsAdd with a Percentage value of `rate`
Ok(build_order_discount(rate, field_value(&config.label)))
}Step 4: Deploy and let merchants edit
Run shopify app deploy, create the metaobject entry, and the merchant can now adjust tier_threshold and tier_rate in the admin. Each checkout reads the live values. To change behavior, edit the entry — no rebuild, no redeploy. For the full Discount API surface, see Shopify's Discount Function API reference.
Beyond discounts: validation and market-aware logic

The pattern generalizes well past discounts because metaobject access works across all function targets — discounts, cart transforms, delivery customization, fulfillment constraints, and validation.
Rule-based cart validation
A validation Function can read an app-owned metaobject holding rules — maximum quantity per SKU, restricted shipping regions, or B2B-only products — and return errors when the cart breaks them. The rules live in entries the merchant manages, so adding a new restricted product is a data edit, not a deploy.
Market-aware behavior
Model one metaobject entry per market, each carrying its own thresholds and rates, and reference the entry that matches the cart's context. Because references are first-class, you can keep a clean one-entry-per-market table and resolve the right one at runtime. This is the kind of dynamic, data-driven logic that was painful to express with a single flattened metafield.
Common Pitfalls
Even a clean pattern has sharp edges. These are the ones that cost the most debugging time.
Forgetting the $app: prefix
The number-one mistake: pointing your input query at a merchant-created metaobject type and getting nothing back. Only app-owned types (the `$app:` reserved prefix) resolve inside Functions. If your query returns null and you swear the entry exists, check the type string first. A standard discount_tiers type will silently fail where $app:discount_tiers works.
Blowing the complexity budget
With each field costing 3 points against a 30-point ceiling, a wide metaobject or several metaobjects in one query will overflow fast. Request only the fields the logic actually reads. If you need many fields, reconsider the data model rather than cramming everything into one query.
Treating values as typed
Every metaobject field value arrives as a string. Numbers, booleans, and dates are stringified. Parse defensively — parseFloat, unwrap_or, sensible defaults — and never assume a field is populated. A missing or malformed value should fail safe to "no discount," not panic.
Not failing safe
A Function that errors at checkout is worse than one that does nothing. Always handle the missing-metaobject case and bad input by returning empty operations. The example above bails out cleanly whenever config is absent or the rate is non-positive.
Best Practices for Shopify Functions Metaobjects

A few habits keep these builds maintainable as they grow.
- Model data deliberately. Use one entry per logical record (per tier, per market) and references between them, rather than one giant JSON field. You get the editing UX and validation for free.
- Namespace with `$app:` from day one. Define types app-owned even during prototyping so you do not refactor later.
- Keep queries lean. Pull only the fields the run logic consumes; the 30-point budget rewards restraint.
- Validate at the edges. Parse strings into typed values once, with defaults, near the top of the run function.
- Document the contract. Write down which metaobject fields the Function depends on so a merchant edit does not break logic silently. Pair this with strong logging in development.
For more on structured data fundamentals, our metafields walkthrough and the broader Shopify development category are good next stops. Shopify's own metaobjects documentation covers definitions and field types in depth.
FAQ
Do Shopify Functions metaobjects work with every function target? Yes. As of the 2026-04 API version, metaobject access is available across all function targets, including discounts, cart transforms, delivery customization, fulfillment constraints, and validation.
Can a Function read any metaobject in the store? No. Only app-owned metaobject types — those using the reserved $app: prefix — are accessible inside a Function. Standard merchant-created types are not readable from Functions.
How much complexity does reading a metaobject cost? Each metaobject root costs 1 point and each field(key:) call costs 3 points, against a total input query budget of 30 points. One metaobject with three fields costs 10 points.
What API version do I need? GraphQL API version 2026-04 or later, released April 1, 2026. Set your Function's schema to that version and regenerate types.
Should I still use metafields for Function config? For a single value or a simple flag, a metafield is fine. Reach for metaobjects when you have multiple related records, want a real editing UX for merchants, or need references between records.
Do I redeploy the Function when rules change? No — that is the whole point. Once the Function reads from a metaobject, merchants edit the entry in the admin and the next checkout uses the new values. The binary stays put.
Conclusion
Combining Shopify Functions metaobjects turns a static WebAssembly binary into a live rules engine: the Function stays simple while the data that drives it lives in structured, merchant-editable entries. The 2026-04 change — app-owned metaobject access inside input queries — is what makes market-aware discounts, tiered pricing, and rule-based validation practical without redeploys. Mind the $app: prefix, respect the 30-point complexity budget, parse values defensively, and always fail safe. Get those four right and you have a pattern that scales from one discount to an entire data-driven catalog.
Building something with Functions and Metaobjects? **Join the Talk Shop developer community and share your Functions + Metaobjects builds with other devs** — the edge cases are where the real learning happens. What is the first piece of logic you would move out of your binary and into a metaobject?
---
Sources:
- Metaobject access in Shopify Functions — Shopify developer changelog
- How to Migrate Shopify Scripts to Functions (2026 Edition) — revize.app
- Mastering Metaobjects in Shopify: Solving Discount Function Dilemmas — entaice.com

About Talk Shop
The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.
