Shopify App Tools & Intents: Navigating 'Tool Not Found' Errors After Full-Page Redirects
Hey there, fellow store owners and app developers!
I wanted to share some really valuable insights from a recent deep dive in our community forums. We’ve been seeing a tricky issue pop up for developers building apps on Shopify, specifically when using the newer shopify.tools API in conjunction with admin.app.intent.link for full-page navigations. It's a classic head-scratcher: you navigate to your app, try to call a tool, and then BAM! "Tool not found."
One of our community members, blueliner, brought this exact problem to the table. They were working on a sidekick-import feature, where an intent would trigger a full-page navigation to their app from another embedded app (like an eBay Importer). The navigation would happen, Sidekick would immediately try to call their registered tool (preview_amazon_product), but the tool handler was never invoked. Instead, they’d just get that frustrating "tool not found" message.
The Initial Suspect: Timing is Everything (or So We Thought!)
When issues like this pop up, especially with new APIs and full-page reloads, the first thing many of us think about is timing. And for good reason! Our expert, Mindaugas_LM, quickly jumped in, pointing out that a full-page navigation means your app reloads from scratch. Sidekick might be firing that preview_amazon_product call as soon as your page starts loading, but your shopify.tools.register(...) call might be running much later in your JavaScript lifecycle. Think about it: after App Bridge initializes, your framework (React, Vue, etc.) mounts, and maybe even after some async data fetches. If the call arrives before the tool is registered, it's a "tool not found" scenario. It's like calling a friend before they’ve even picked up their phone! ![]()
Mindaugas highlighted what he called the "smoking gun": blueliner's logs showed their tool registration was happening at ~2380ms from page load. That 2380ms? That’s typically your app's bundle parse and evaluation time. Even if you place register at the very top of your entry file, your entire bundle (with all its imports) still needs to download and parse before that registration can actually execute. This creates a race condition where Sidekick can easily win.
Solving the Timing Race: Early, Decoupled Registration
To tackle this potential timing issue, Mindaugas offered a clever and robust solution: register your tool outside your main app bundle, in a tiny inline <script> tag right after the App Bridge CDN tag in your <head>. This way, your tool's existence is known almost immediately, decoupling "tool exists" from "tool is ready to work."
Here’s the pattern he suggested:
Then, once your main app bundle (with all your Redux/API logic) has fully booted, you'd call window.__resolveAppReady({ previewAmazonProduct: ... }). This pattern ensures that if Sidekick calls your tool before your app is fully ready, the call is buffered by the promise and handled once the app is up, rather than being dropped.
Mindaugas also suggested adding high-resolution timestamps (performance.now()) to diagnose whether this fix worked. If registration moved to sub-100ms and the issue disappeared, it was indeed a timing race.
Unmasking the Real Culprit: A Session Mismatch
Here’s where it gets interesting, and where blueliner’s persistence really paid off. They tried all of Mindaugas’s excellent suggestions: inline registration, promise-based buffering, high-res timestamps, logging their shopify.tools guard – everything. And what did they find?
It wasn’t a timing/race issue after all! Even with registration happening in under 100ms, the cross-app path still failed. Their conclusion: "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."
This is a crucial distinction. If Sidekick is calling into a different App Bridge session, then no amount of optimizing your registration order or speed will fix it. Your tool simply isn't registered in that particular session. Blueliner rightly identified this as a "platform-level limitation" for now.
The Community's Practical Workaround
So, what do you do when you hit a platform-level limitation? You find a clever workaround! Blueliner's solution is both simple and effective:
- The
admin.app.intent.linkstill opens your app's page as intended. - However, the tool itself doesn't immediately try to do the actual work. Instead, once your app's page is fully loaded and initialized, then the tool performs its intended function.
This approach works reliably because by the time the actual work is initiated, both the tool registration and the Sidekick call are guaranteed to be operating within the same App Bridge session. It effectively sidesteps the session mismatch by delaying the critical operation until the environment is stable and consistent.
Key Takeaways for Your Shopify App
This discussion really highlights the nuances of building robust apps on an evolving platform like Shopify. Here’s what we can learn from blueliner’s journey:
- Always Prioritize Early Registration: Even if it wasn't the ultimate fix here, Mindaugas’s advice to register your
shopify.toolsas early as physically possible (preferably with an inline script and promise buffer) is a best practice. It mitigates timing races that are very common. - Understand Full-Page Navigations: Be aware that full-page navigations, especially from other embedded apps, can lead to fresh App Bridge sessions. This can introduce complexities not present in same-page or client-side transitions.
- Diagnose Methodically: Use tools like
performance.now()and thorough logging to truly understand *when* things are happening. Sometimes, what looks like a timing issue is actually a deeper architectural one. - Embrace Workarounds: If you hit a platform-level limitation that’s beyond your control, a well-placed workaround can be your best friend. Blueliner's "navigate then work" pattern is a great example of adapting to platform behavior.
It’s a testament to the power of our community that developers like blueliner and Mindaugas share these detailed analyses and solutions. It helps us all build better, more resilient apps for Shopify merchants. Keep these insights in mind as you develop your own tools and integrations! ![]()