Skip to content

Engineering

We asked Shopify for a new Sidekick intent type. They shipped it.

By Jahangir Alam · August 31, 2026 · 9 min read

How to get an application/* intent type added for your category - and what we got wrong about intents on the first attempt.

If you build a Shopify app whose core object isn't a product, order or customer, you've probably hit this wall: Sidekick app extensions use a fixed intent vocabulary, and your thing isn't in it.

When we started wiring QuotWay into Sidekick, the supported list was application/{ad, campaign, email, faq, loyalty-program, return, review, shipment, ticket} plus shopify/{customer, order, product}. We build a B2B quote and negotiation app. There was no quote.

That isn't a soft limit you can work around. shopify app deploy is atomic and rejects an unsupported intent type outright - so one bad intent doesn't fail alone, it takes your whole app version with it. We shipped read-only data tools and parked the rest. (That first phase is written up separately in building Shopify Sidekick app extensions.)

Then we asked. Shopify added application/quote to the ecosystem - for every quote app, not just ours.

This is what we learned, including the part we had modelled completely wrong.

There is a front door, and it's a public repo

Intent types are governed in the open at Shopify/app-intent-types. You open a discussion, make your case, and a platform engineer answers.

What made ours land, in rough order of weight:

Argue for the category, not your app. We asked for quote because quote and negotiation apps are a recognisable class of Shopify app with a shared object, not because QuotWay needed it. A type added for one vendor is a maintenance liability; a type that serves a category is a platform feature. Write the request the way Shopify would have to justify it internally.

Bring the real object. We described what a quote actually is - a buyer request that gets priced, negotiated over several rounds, and converted into an order - and which fields carry meaning across every app in the category. That question ("what does the schema need to hold?") is the one that has to get answered before anything ships, so answering it up front removes the main reason to defer.

Propose concrete actions. We proposed open, send_proposal, counter and convert. Every one was declined. That was still the most useful part of the RFC, because the reasons they were declined taught us the model - see the next section. A wrong concrete proposal beats a vague one: it gives the platform team something specific to correct.

Offer to do the work. We offered to open the follow-up PR adding the types/ entry. They declined and did it themselves, but the offer signals you'll maintain what you're asking for.

Total elapsed: RFC filed early July, approved 27 August. Slower than a feature you control; far faster than building around the gap forever.

The thing we got wrong: an intent is a handoff, not a command

Here's the reply that reframed it, from the platform engineer who approved the type:

"We're not adding open, send_proposal, counter or convert: every application/* type supports precisely create and edit. An intent is a handoff, not a command. Sidekick's job ends when the merchant is in your app with the right context loaded, and your UI owns the rest."

We had modelled intents as a remote-control API - one intent per verb our product supports. That's wrong, and once you see why, the whole surface gets simpler.

Two consequences worth internalising:

open is already what edit means. There's no separate "just show it" action, because edit is a navigation contract, not a mutation. You register edit:application/quote and Sidekick lands the merchant on your page. Whether that page is an editor or a read-only view is your business. Shopify's own reference extensions are literally named open-email and open-campaign - and both register action = "edit".

Your verbs aren't intents, they're in-page tools. send_proposal, counter and convert didn't get dropped, they got relocated. They're tools that run once the quote is open and stage into your own confirm UI. Which is where the merchant was always going to press the button anyway. We'd been trying to hoist our entire product surface into the intent vocabulary; the platform is telling you not to.

If you're about to file an RFC asking for six verbs, save yourself a round trip: ask for the type, and design the verbs as tools.

The half that's easy to miss

Registering the intent does nothing on its own. Both of these have to be true:

  1. You register an intent for application/quote.
  2. Your search results carry mimeType: "application/quote".

"A resource link's mimeType matching an intent type is what makes a search result invokable."

That's the connection. Your data extension returns resource links; a link whose mimeType matches a registered intent becomes something the merchant can open, rather than a card they can only read.

We'd been emitting a vendor mime - application/vnd.quotway.quote - which was correct while no standard type existed, and became the thing blocking us the moment one did. The fix is two strings:

-const QUOTE_MIME = "application/vnd.quotway.quote";
+const QUOTE_MIME = "application/quote";

-  uri: `gid://application/quotway.quote/${q.id}`,
+  uri: `gid://application/quote/${q.id}`,

Keep vendor mimes for everything with no intent type. Our pipeline summaries and targeting rules still use application/vnd.quotway.*, because you can't open a pipeline summary. Marking them with a standard type would promise the merchant an action that doesn't exist.

The wiring, concretely

An intent extension is an admin_link, not a ui_extension:

[[extensions]]
name = "Open quote"
description = "Open a specific QuotWay quote, quote request (RFQ), proposal or counteroffer…"
handle = "quotway-open-quote"
type = "admin_link"

  [[extensions.targeting]]
  target = "admin.app.intent.link"
  url = "/app/quotes/{id}"
  tools = "./tools.json"
  instructions = "./instructions.md"

    [[extensions.targeting.intents]]
    type = "application/quote"
    action = "edit"
    schema = "./intent-schema.json"

And the schema, which is where the URL actually gets filled in:

{
  "$schema": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/intent.json",
  "value": {
    "type": "string",
    "description": "The GID of the quote to open.",
    "mapTo": "param",
    "fieldName": "id"
  },
  "inputSchema": {
    "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/application/quote.json",
    "type": "object",
    "properties": {
      "buyer_email": { "type": "string", "description": "…" },
      "company":     { "type": "string", "description": "…" },
      "total":       { "type": "number", "description": "…", "minimum": 0 },
      "message":     { "type": "string", "description": "…" }
    }
  }
}

Four things that will save you time:

mapTo: "param" + fieldName fills the {id} in your url. The names don't have to match - fieldName is the redirect.

GIDs get truncated for you. When a value mapped to a param is a GID, Sidekick substitutes only the segment after the final /. So gid://application/quote/clx123abc lands as clx123abc. Our existing route already expected that id, so no route change - but it does mean the tail of your GID must be whatever your router wants.

Published schemas are deliberately minimal. application/quote publishes id with additionalProperties: true. Everything domain-specific - buyer_email, company, total, message - you declare yourself under inputSchema.properties. The standard type is a handshake, not a data model.

Tools are required, not optional. Sidekick only invokes intents that have tools. An intent-link target with no tools.json registers fine and never fires.

Two traps

api_version is not allowed on an admin_link extension. We copied it from our ui_extension TOMLs and the config was invalid. Caught by:

shopify app config validate --json

Run that before every deploy. Deploy is atomic - an invalid extension doesn't fail by itself, it fails the version.

The budget is tighter than it looks: 5 intents, and 20 tools shared across every extension you ship. Our data extension already registered 15 tools. Adding three in-page staging tools plus one prefill tool put us at 19 of 20. If you're planning a rich data extension and actions, budget the tools first - you will run out of tools long before you run out of intents.

If you're in this position

You probably are, if your app's core object isn't in the list. Roughly:

  1. Check Shopify/app-intent-types - someone may have asked already.
  2. Open a discussion for your category, not your app. Describe the object, the fields that generalise, and what merchants ask for out loud.
  3. Ask for the type; design your verbs as tools. create and edit are what you get. That's enough.
  4. Ship the data extension meanwhile. Read-only tools don't touch the intent vocabulary, so they're never blocked. When your type lands, changing the mime is a two-line diff.
  5. Validate before you deploy.

The vocabulary looks like a wall. It's a queue.


QuotWay is a B2B quote and negotiation app for Shopify. The application/quote intent type is available to every app in the category - if you build one, it's yours too. If you're a merchant rather than a developer, the merchant-facing write-up covers what this actually does in the admin.

Related articles

See how QuotWay handles this on your store.

We’d like to set analytics cookies to understand how the site is used. They’re not required — declining changes nothing about how the site works, and you can change your mind any time on our privacy page.