Admin.app.intent.render: the surface doesn't close and Sidekick says nothing after intents.response.ok resolves

api_version = "2026-04", Shopify CLI 4.8.0, app 6981071, dev store gw-qa-test-77.myshopify.com.

[[extensions.targeting]]
module = "./src/IntentExtension.tsx"
target = "admin.app.intent.render"
tools = "./tools.json"
instructions = "./instructions.md"

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

What works. Sidekick invokes the intent, the panel renders, shopify.intents.request.value carries the input, a runtime-registered tool stages the change, the merchant clicks Save and the app writes to its backend.

How I resolve it — following the docs (callback not async, awaited promise handed to event.waitUntil) and the pattern in Shopify/discounts-reference-app PR #78:

onClick={(event) => {
  const p = (async () => {
    await writeToBackend();
    await shopify.intents.response.ok({ setting, target });
  })();
  event.waitUntil?.(p);
}}

The promise returned by response.ok() resolves. I record that in telemetry, so delivery is confirmed rather than assumed — 4 of 4 sessions delivered.

Two things then don’t happen:

  1. The surface stays on screen. The docs say “The invoking surface closes the modal, so you don’t need to navigate yourself”, but this target is described as rendering inline. Is closing expected for an inline surface, or is the panel meant to remain in the conversation? response.closed() behaves the same way.

  2. Sidekick says nothing afterwards — 0 of 5 sessions. The only message the merchant sees is the one built from the tool’s staging result, and it appears before they click Save. Is the invoking turn expected to resume and surface response.data, or does resolution hand control back with no merchant-visible effect? If it’s the latter, the extension surface is the only place an outcome can be shown, which changes how these panels should be designed.

Also: shopify.intents.hasProvider exists at runtime but appears in neither the docs nor @shopify/ui-extensions@2026.4.0, whose Intents interface carries only launchUrl and invoke — no request, no response. What does it report? It looks like it would answer question 2 directly.

Runtime keys observed on this target:

intents.launchUrl, intents.invoke, intents.hasProvider, intents.request, intents.response
response.ok, response.error, response.closed

Update: same result with admin.app.intent.link

To rule out the target, I built the same flow on admin.app.intent.link instead of admin.app.intent.render. The link opens our app’s existing editor page (App Bridge). The proposal from Sidekick is prefilled into the form, the merchant saves there, and then the page calls shopify.intents.response.ok({ id, setting, target, value }), matching our declared outputSchema.

Result: the save succeeds, ok() resolves, and Sidekick still reports the activity as pending. It never receives the result. So neither target delivers it for us.

What our two attempts have in common, compared with the public working examples:

  • Intent type. We use application/loyalty-program (edit). Both working examples I could find (discounts-reference-app PR #78 and a public Only Bundles integration) use shopify/* types.
  • id in ok(). Ours is our app’s own record id (a 24-hex string). The working examples return a Shopify GID.

One more observation: Sidekick sometimes passes the values in request.value.data and sometimes opens the intent with data: {}, then (we assume) expects the tool to deliver them.

Questions:

  1. Is intents.response.ok() currently delivered to the Sidekick conversation for application/* intent types?
  2. If yes, what should id be for an app-owned resource that has no Shopify GID?
  3. Is there a way to check on our side whether the host accepted the output, for example a log in the dev dashboard?