Problem: I’m implementing sidekick-import with admin.app.intent.link. When Sidekick invokes the intent from another app, it triggers a full-page navigation to my app. After the redirect, Sidekick immediately calls my registered tool (preview_amazon_product), but gets “tool not found” error.
Hi ![]()
This looks like a registration-vs-call timing race, which is easy to hit specifically because it’s a full-page navigation rather than a client-side transition.
On a full-page nav your app reloads from zero. Sidekick fires preview_amazon_product as soon as it lands — but your shopify.tools.register(...) call runs later in your JS lifecycle (after App Bridge init, after your framework/router mounts, maybe after an async data fetch). So the call arrives before the tool exists → “tool not found.” It’s the classic “handler attached after the event already fired.”
A few things I’d check/try, roughly in order:
- Register as early as physically possible. Move the
registercall to the very top of your entry file, right after App Bridge is available — before React/Vue mounts, before routing, before anyawait. Registration should be one of the first things that runs, not something a component does in an effect on mount. - Make sure App Bridge is the Shopify-hosted script in
<head>, loaded first (not bundled/deferred). Ifshopifyisn’t ready synchronously on that fresh load, your register call gets pushed later and you lose the race. - Confirm the intent’s landing route actually runs your registration code. With a deep-link/intent you may land on a specific route — if that route doesn’t execute the same bootstrap that registers your tools, the tool genuinely won’t be there. Register tools app-wide at bootstrap, not per-page.
- Decouple “registered” from “ready.” If the reason you register late is that the tool needs data/context that loads after navigation, register a thin wrapper immediately and have the handler
awaitwhatever it needs (return a promise). That way the tool exists at call time even if its dependencies are still loading — instead of delaying the registration itself. - Sanity-check the name string (
preview_amazon_product) matches exactly between register and the intent call — casing/typos included.
Honest caveat: the tools/Sidekick intent API is fairly new, so I’m not 100% sure whether Shopify queues an incoming tool call until registration or drops it immediately — the behaviour you’re seeing suggests it’s not queued, which is why register-order matters so much here.
To give you a more precise answer: where does your register call actually run — top of the entry file, or inside a component/effect? And is App Bridge the CDN <script> in <head>, or bundled? If you can paste the minimal bootstrap (the register call + how App Bridge loads), I can point at the exact spot.
Here’s our setup:
App Bridge: CDN script in , loaded first:
Tool registration: Top of entry file (index.js), module level, before React mounts:
import {previewProductApi} from "./api/api";
import store from "./store";
import {previewPopupChange} from "./store/action-creator";
// Runs at module load — before React render
if (typeof shopify !== 'undefined' && shopify.tools) {
shopify.tools.register('preview_amazon_product', async (input) => {
const result = await previewProductApi(input.amazonProductUrl);
// ... dispatch to Redux store
return {ok: true, title: result.title};
});
}
// React mounts after
const root = createRoot(document.getElementById('root'));
root.render();
Logs confirm: shopify is available, tool registers at ~2380ms from page load.
What works:
- Merchant is on any Shopify Admin page (Products, Orders, Customers, etc.) - intent fires, tool is called, everything works perfectly
- Merchant is already inside our app - tool works perfectly
What fails:
- Merchant is inside another embedded app (e.g. eBay Importer) - intent navigates to our page, tool registers successfully (confirmed via logs), but tool handler is never invoked. Sidekick returns “tool not found”.
That 2380 ms is the smoking gun. ![]()
“Registers successfully” only tells you registration eventually ran — not that it ran before Sidekick’s call arrived. Your two working cases (already in-app, or on a native Admin page) simply give more slack before the tool is invoked. The cross-app intent path almost certainly dispatches the call much earlier than 2.38 s, so on that one path the call lands, finds nothing registered yet, and you get “tool not found.” The later successful registration in your logs is a different (too-late) moment.
2380 ms is basically your bundle parse/eval time. Even though register is at module top “before React,” the whole index.js (plus its ./api, ./store, ./action-creator imports) has to download + parse first. That’s your race window.
The fix: register outside your bundle, in a tiny inline <script> in <head>, right after the App Bridge CDN tag. Make it a thin wrapper that doesn’t need your app to be booted:
<script src="https://cdn.shopify.com/.../app-bridge.js"></script>
<script>
// Runs in <100ms, not 2380ms. Tool exists almost immediately.
window.__appReady = new Promise((res) => { window.__resolveAppReady = res; });
shopify.tools.register('preview_amazon_product', async (input) => {
const app = await window.__appReady; // wait for the real app if it isn't up yet
return app.previewAmazonProduct(input); // do the actual work once booted
});
</script>
Then in your bundle, once Redux/api are ready, call window.__resolveAppReady({ previewAmazonProduct: ... }). This decouples “tool exists” (instant) from “tool is ready to work” (2.38 s later) and buffers a call that lands pre-boot instead of dropping it.
One important diagnostic to settle it: add high-res timestamps (performance.now()) at (a) the inline register line and (b) when your handler actually fires. Then test the cross-app path again:
- If moving registration into
<head>fixes it → it was purely the race, done. - If it still fails even with sub-100 ms registration → then it’s not timing, it’s a session/instance mismatch: the App Bridge context Sidekick is calling into after the cross-app hop isn’t the same one your fresh page registered on. That’s a platform-level behaviour, and I’d take that repro straight to Shopify (Partner support / a dev-platform bug report), because no amount of register-ordering fixes a handoff to a different App Bridge session.
Small aside: your guard if (typeof shopify !== 'undefined' && shopify.tools) will silently skip registration if shopify.tools isn’t populated yet at module-eval on some entry paths — worth logging both branches so you’re sure it’s truthy on the failing path specifically.
My bet is the inline-<head> registration fixes the working-but-slow cases; if the cross-app hop still drops it, you’ve now got a clean, minimal repro that proves it’s Shopify’s side.
Thanks for the detailed analysis! We tried all the approaches:
- Inline in — registered tools immediately after App Bridge CDN tag with a promise-based pattern to buffer calls until the app boots. Registration happens in under 100ms, but the cross-app path still fails.
- High-res timestamps — confirmed that registration completes well before Sidekick’s call arrives on same-page scenarios, but on cross-app navigation the call never reaches our handler at all.
- Logging both branches of the shopify.tools guard - it’s always truthy, registration always runs.
So it’s not a timing/race issue. The tool is registered, but after a cross-app hop Sidekick calls into a different App Bridge session/instance than the one we registered on. No amount of register-ordering fixes that.
We’ve accepted this as a platform-level limitation for now. Our workaround: the intent opens the page, and the tool does the actual work once the page is loaded — works reliably since by that point registration and the Sidekick call are on the same session.