Shopify extensions
Apps/Shopify/Shopify extensions

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:

typeWhat it isBuilt
themeApp blocks and app embeds for the theme editor (below, and Storefront blocks)No: Liquid and assets as written
ui_extensionAdmin, checkout, customer account and POS surfaces, and Sidekick tools on admin targetsesbuild, into one script
function, or an older per-API typeShopify Functions: discounts, cart transforms, delivery and payment customizations, validations, fulfillment constraints, order routing and pickup optionsThe Shopify CLI, into WebAssembly (JavaScript or Rust)
web_pixel_extension, checkout_ui_extension, checkout_post_purchase, pos_ui_extension, product_subscriptionWeb pixels and the types built from one src/index fileesbuild, into one script
flow_action, flow_trigger, flow_templateShopify Flow actions, triggers and templatesNo: configuration
tax_calculation, editor_extension_collection, admin_link, channel_config, order_attribution_config, flow_trigger_lifecycle_callbackConfiguration with a few files, nothing to buildNo

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 a shopify.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 theme puts 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 its name, 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.
  • uid is 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/*.json are optional; if there are any, exactly one is the default (en.default.json).
  • channel_config and order_attribution_config are 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 = true

Files 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.sidekickSummary in settings.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 fetch the app's own routes; https://extensions.shopifycdn.com is always allowed in CORS, so routes need nothing for it. Each call is an extension event 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 a package.json and the schema.graphql that shopify app function schema writes.
  • Rust, or anything else: set [extensions.build] command and path, where the wasm lands (dist/index.wasm unless set). A cargo command gets Rust (with the wasm32-unknown-unknown and wasm32-wasip1 targets) 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 query

Web 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 under settings; the old configuration isn'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 (or webPixelUpdate when that answers TAKEN) in onSuccess of shopifyShop's install and reinstall. That needs the write_pixels and read_customer_events scopes.
# 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.ts

Flow

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's return_type_ref and schema go 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 up https://. A custom configuration page needs config_page_url, config_page_preview_url and validation_url together.
  • A template needs a description (at most 1,024 characters), and [extensions.template] with its module (the .flow file, in the folder) and categories from 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

typeNeedsShips
tax_calculationproduction_api_base_url, calculate_taxes_api_endpointIts configuration
editor_extension_collectionA name; includes lists the handles it groupsIts configuration
admin_linkOne targetIts tools, instructions and intent schemas, as a UI extension's
channel_configOne per appspecifications/ (.json .toml .yaml .svg)
order_attribution_configOne per appicons/*.svg
flow_trigger_lifecycle_callbackIts url, resolved as a Flow action'sIts 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.