Shopify extensions
Everything under extensions/ ships with the app's Shopify app, the way shopify app deploy ships it: theme blocks, UI extensions, Functions, Flow, web pixels, Sidekick tools and the configuration-only types. Inference Bureau builds what needs building and releases it with the app's configuration.
What's released
An extension's type in its shopify.extension.toml decides how it's read, built and released:
| type | What it is | Built |
|---|---|---|
theme | App blocks and app embeds for the theme editor (below, and Storefront blocks) | No: Liquid and assets as written |
ui_extension | Admin, checkout, customer account and POS surfaces, and Sidekick tools on admin targets | esbuild, into one script |
function, or an older per-API type | Shopify Functions: discounts, cart transforms, delivery and payment customizations, validations, fulfillment constraints, order routing and pickup options | The Shopify CLI, into WebAssembly (JavaScript or Rust) |
web_pixel_extension, checkout_ui_extension, checkout_post_purchase, pos_ui_extension, product_subscription | Web pixels and the types built from one src/index file | esbuild, into one script |
flow_action, flow_trigger, flow_template | Shopify Flow actions, triggers and templates | No: configuration |
tax_calculation, editor_extension_collection, admin_link, channel_config, order_attribution_config, flow_trigger_lifecycle_callback | Configuration with a few files, nothing to build | No |
Older function types are still taken as Shopify names them: order_discounts, product_discounts, shipping_discounts, cart_checkout_validation, cart_transform, delivery_customization, payment_customization, fulfillment_constraints, order_routing_location_rule, local_pickup_delivery_option_generator and pickup_point_delivery_option_generator. subscription_management is read as product_subscription.
Folders and handles
- Each extension is a folder
extensions/<folder>/with ashopify.extension.toml. The folder name is lowercase letters, digits and dashes, starting with a letter or digit; files outside such a folder are ignored. - Every type but
themeputs its settings on one[[extensions]]table (keys at the top of the file count too, as the CLI merges them). A toml with none, or more than one, isn't released. - The handle is the toml's
handle, else one made from itsname, else the folder's name (a UI extension or function skips the name and uses the folder). It's at most 50 lowercase letters, digits and dashes. A theme extension's handle is always its folder. uidis optional: without one, each extension gets a stable one from the Shopify app and its handle, so each release updates the same extension instead of adding another. Renaming the handle makes a new extension on Shopify.- UI extensions and Functions need
api_version(api_version = "2026-10"). locales/*.jsonare optional; if there are any, exactly one is the default (en.default.json).channel_configandorder_attribution_configare one per app: their type is their handle, whatever the folder is called.
How they're released
Extensions go out with the Shopify app's configuration, never by hand: development's after each sync that changes extensions/ (as shopify app dev would), production's when a deploy goes live. UI extensions, Functions and single-script types are built first, in a sandbox kept per app, with the extension's own package.json dependencies installed once per set (from package-lock.json when there is one) and an unchanged extension's last build reused. The first build takes a minute or two; later ones rebuild only what changed.
One extension that can't be read or doesn't build stays out; the rest release. Its reason (or the build's error, with the file and line) shows in Problems, and each release is a shopify event in Logs naming what shipped and what was skipped. A release Shopify refuses shows in Problems too, and the store keeps the extensions it had.
Theme app extensions
type = "theme", at the top of the toml. It ships only blocks/ and snippets/ (.liquid), assets/ (.js .css .json .png .jpg .svg .wasm) and locales/ (.json), one level deep; other files are left out and reported. One with nothing in blocks/ isn't released. How blocks get their data and call the app is on Storefront blocks and app proxy.
UI extensions
type = "ui_extension": Preact (or React) code for the admin, checkout, Thank you and Order status pages, customer accounts and POS. Each [[extensions.targeting]] needs a target and a module that's in the folder, and a should_render needs its module too. The folder needs a package.json naming its dependencies (preact and @shopify/ui-extensions). All the targets build into one script, as the Shopify CLI builds it; a target's should_render module builds into its own beside it. Checkout, admin and POS UI extensions are the same type with other targets.
# extensions/wishlist-account/shopify.extension.toml
api_version = "2026-10"
[[extensions]]
name = "Wishlist"
handle = "wishlist-account"
type = "ui_extension"
[[extensions.targeting]]
target = "customer-account.page.render"
module = "./src/WishlistPage.tsx"
[extensions.capabilities]
network_access = trueFiles a target names (tools, instructions, assets, each intent's schema) must be inside the folder and ship beside the script. A customer account, checkout or admin extension calls the app at process.env.IB_EXTENSION_URL with its session token; requests reach routes under web/app/extension/ (Customer account extensions).
Calling out needs network_access = true under [extensions.capabilities], and Shopify requires that capability to be approved for the app in the Partner Dashboard first.
Sidekick tools
A UI extension (or an admin_link) gives Sidekick tools with a tools file on its targets: a JSON array of tools, each with a name (at most 64 characters) and a description (at most 512) that Sidekick reads to pick it. An extension with tools needs a description in its toml naming what they cover. Tools are a UI extension on the admin.app.tools.data target, whose module registers each one with shopify.tools.register(name, handler).
- Sidekick takes at most 20 tools from an app, across all its extensions. With more, the release reports it.
- Shopify takes tools only with the app's summary of them:
shopify.sidekickSummaryinsettings.ib.ts, a sentence of at most 1,000 characters. Without it, extensions with tools are held back and the rest release. - Handlers only read data and should answer within about a second. They
fetchthe app's own routes;https://extensions.shopifycdn.comis always allowed in CORS, so routes need nothing for it. Each call is anextensionevent in Logs.
# extensions/reviews-admin/shopify.extension.toml
api_version = "2026-10"
[[extensions]]
name = "Reviews"
handle = "reviews-admin"
type = "ui_extension"
description = "Find a product's reviews, their ratings and the ones waiting for moderation"
[[extensions.targeting]]
target = "admin.app.tools.data"
module = "./src/tools.ts"
tools = "./tools.json"
instructions = "./instructions.md"Functions
Functions are built to WebAssembly by the Shopify CLI's own app function build, so they come out as shopify app deploy would make them. Each target needs a target; its input_query, when named, must be in the folder (an older single-target function can keep input.graphql beside the toml). Input variables go on the extension or on its targets, not both.
- JavaScript or TypeScript: no
[extensions.build] command, with apackage.jsonand theschema.graphqlthatshopify app function schemawrites. - Rust, or anything else: set
[extensions.build] commandandpath, where the wasm lands (dist/index.wasmunless set). Acargocommand gets Rust (with thewasm32-unknown-unknownandwasm32-wasip1targets) installed the first time, and cargo's cache is kept, so later builds take seconds. Other languages' toolchains aren't there.
# extensions/bundle-discount/shopify.extension.toml
api_version = "2026-10"
[[extensions]]
name = "Bundle discount"
handle = "bundle-discount"
type = "function"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart-lines-discounts-generate-run"
# Files: package.json, schema.graphql, src/*.ts (or .js), the input queryWeb pixels and single-script types
Web pixels, checkout UI extensions (the older type), post-purchase, POS UI and subscription management extensions are each one src/index.ts (or .tsx, .js, .jsx) bundled into a script, with a package.json naming the type's package (@shopify/web-pixels-extension, @shopify/checkout-ui-extensions, @shopify/post-purchase-ui-extensions, @shopify/retail-ui-extensions, @shopify/admin-ui-extensions). Checkout and POS UI extensions need a name.
- A web pixel needs
runtime_context("strict"), and its settings go undersettings; the oldconfigurationisn't taken. - A web pixel's built script is at most 128 KiB, Shopify's limit. A bigger one stays out, with its size in Problems.
- Releasing a pixel isn't enough: a store runs it only once the app connects it, with
webPixelCreate(orwebPixelUpdatewhen that answersTAKEN) inonSuccessofshopifyShop's install and reinstall. That needs thewrite_pixelsandread_customer_eventsscopes.
# extensions/analytics/shopify.extension.toml
name = "Analytics"
type = "web_pixel_extension"
runtime_context = "strict"
[customer_privacy]
analytics = true
marketing = false
preferences = false
sale_of_data = "disabled"
[settings]
type = "object"
[settings.fields.accountId]
name = "Account ID"
type = "single_line_text_field"
# Files: package.json (@shopify/web-pixels-extension), src/index.tsFlow
Flow actions, triggers and templates are configuration, checked as the CLI checks them. Each needs a name.
- A trigger or action declares its fields under
[[extensions.settings.fields]];schema, when named, must be in the folder, and an action'sreturn_type_refandschemago together. - An action needs a
runtime_url. A URL starting with/is the app's own, resolved against its URL in each environment (so development's and production's actions each reach their own); every URL must end uphttps://. A custom configuration page needsconfig_page_url,config_page_preview_urlandvalidation_urltogether. - A template needs a
description(at most 1,024 characters), and[extensions.template]with itsmodule(the.flowfile, in the folder) andcategoriesfrom Shopify's list.
# extensions/review-posted/shopify.extension.toml
[[extensions]]
name = "Review posted"
type = "flow_trigger"
handle = "review-posted"
description = "A customer posted a review"
[[extensions.settings.fields]]
type = "product_reference"
[[extensions.settings.fields]]
type = "number_integer"
key = "Rating"
# A flow_action needs runtime_url = "/flow/notify" (resolved against the app's URL; must end up https://).Configuration-only types
| type | Needs | Ships |
|---|---|---|
tax_calculation | production_api_base_url, calculate_taxes_api_endpoint | Its configuration |
editor_extension_collection | A name; includes lists the handles it groups | Its configuration |
admin_link | One target | Its tools, instructions and intent schemas, as a UI extension's |
channel_config | One per app | specifications/ (.json .toml .yaml .svg) |
order_attribution_config | One per app | icons/*.svg |
flow_trigger_lifecycle_callback | Its url, resolved as a Flow action's | Its configuration |
Not released
Payments extensions (payments_extension) aren't released: only apps approved as Shopify Payments Partners can ship them. Any type not listed here is reported on the release ("… aren't released by Inference Bureau yet") rather than left off Shopify without a word.