Embed Migration Runbook
Move one merchant store from script tags and legacy theme code to a theme app embed, and prove it works before you remove anything. This runbook is for any developer or support engineer. You don't need to know the kit's code.
Prepared by David · October 2026 · Embed Migration Kit
1. Who this is for and what you get
Who: a developer or support engineer who migrates merchant stores one at a time. You follow the steps, read what the app shows, and you only call the lead developer in the cases listed in section 7.
At the end of one store you have:
- The app embed switched on in the live theme and running on the storefront (three signals green).
- This app's script tags deleted, or listed as unreachable with the cause.
- A cleanup checklist for legacy theme code (file and line), approved in writing by the merchant.
- If the store sells one variant per country: fixed prices per market, checked with an audit.
- A JSON and a CSV report for the migration board.
How long it takes: the kit's own work for one store takes minutes. Most of the time is waiting for the merchant: the activation click (Stage 2) and the written approval for theme cleanup (Stage 4). The full flow has run end to end on one development store (October 10, 2026). Real merchant timings are not measured yet: write yours in the report.
Why now: three dates, not two.
| Date | What happens |
|---|---|
| October 1, 2026 (passed) | Shopify rejects new script tags on every API version: "This app can't create or update script tags. Use theme app extensions instead." |
| January 1, 2027 | The Admin API rejects non-expiring offline access tokens. |
| March 1, 2027 | Shopify stops injecting script tags. |
2. Before you start
Check every item before you open the app.
- Access to the store. Use a collaborator account, not a shared login. Any theme cleanup is done by a person, on a duplicated theme, with written approval.
- The app is installed on the store, and the app version with the embed is released. A new app version takes several minutes to reach stores.
- Scopes. The app needs
read_script_tags,write_script_tagsandread_themes. For prices by country it also needsread_products,write_productsandread_markets. It never asks forwrite_themes. Keepwrite_script_tagsuntil the audit reads zero script tags in every reachable store. One developer had 107 stores that re-authorized without that scope, and their tags could no longer be deleted. - Markets. The country selector appears only when the store has at least two markets (Settings → Markets). With one market the embed still runs and reports, but there is nothing to choose.
- Written consent from the merchant before any theme cleanup or variant removal. The kit never deletes theme code, variants or products.
- A duplicated, unpublished copy of the live theme, for any cleanup.
3. The five stages for one store
Open the store admin → Apps → Embed Migration Kit. At the top you see a banner "Store status: …" and, on the right, the button Re-scan store. Click Re-scan store after every change you make outside the app.
| Status | What it means |
|---|---|
| Still registered | Not verified yet: at least one of the three signals is missing. |
| Active | Verified. Script tags or legacy theme markers still exist. |
| Removed | Verified, with 0 script tags and 0 legacy markers in the live theme. |
Stage 1 · Measure
Goal: know everything the old integration left behind in this store.
Do:
- Go to the section 1 · Measure.
- Read the three numbers: script tags registered, legacy markers in the live theme and themes scanned.
- Read the table under each theme that has markers: File (file and line), What and Code.
- Optional, for the merchant or your lead: click Ask Shopify to create a script tag (live proof).
You should see:
- Each marker with its file and line. The kit scans
layout/theme.liquid,snippets/*andsections/*in every theme. - After the live proof: a banner "Shopify refused the script tag" with Shopify's own message.
The kit finds these legacy markers:
| What the table says | What it means |
|---|---|
Price wrapped in <span class="money"> for JS conversion | A price that old JavaScript rewrites in the browser. |
Old currency <select id="currencies"> | The old currency picker. |
Old .currency-picker widget | The old picker widget. |
| Theme includes a legacy currency snippet | A {% render %} or {% include %} of the old snippet. |
| Client-side currency conversion library | Old conversion code (for example Currency.convertAll). |
The Script Tag API only returns this app's own script tags. The kit can't see or touch tags from other apps.
Stop if: the live proof shows "Shopify created the script tag". That means the platform behaves differently from what this runbook expects. Escalate (section 7).
Stage 2 · Activate
Goal: the embed is switched on in the live theme.
An app can't switch an embed on by itself. A person must click it in the theme editor, normally the merchant.
Do:
- Go to the section 2 · Activate. The table has one row per theme: Theme, Role (Live or Draft), Embed (settings_data.json), Legacy markers, Last beacon and Action.
- On the Live row, click Activate embed. It opens the theme editor on that exact theme, by its ID, with the embed ready to switch on. If the merchant does this step, send them Template A (section 4) with this link.
- In the theme editor, under App embeds, find Currency switcher (Embed Migration Kit) and switch it on.
- Leave the three settings like this: Position (Bottom left or Bottom right, as the merchant prefers), Neutralize legacy currency code left in the theme: on, Report to the app that the embed is running: on.
- Click Save.
Manual path (if the link doesn't work): Online Store → Themes → Customize → App embeds → "Currency switcher" → On → Save.
You should see: back in the app, after Re-scan store, the Live row shows Enabled and its button changes to Open in editor. The other embed states are Disabled, Not added yet (never switched on) and Unreadable.
Stop if: the editor opens a theme that is not the live one. Close it and use the manual path on the live theme. The kit builds the link by theme ID because the official themes/current link has opened the draft theme after a theme switch.
Remember: the embed lives per theme. If the merchant publishes another theme later, the new theme starts without the embed. The per-theme table shows it: activate again on the new live theme.
Stage 3 · Verify
Goal: prove that the embed is running, not only enabled. "Enabled isn't running."
| Signal | Where it comes from | Green when |
|---|---|---|
| settings_data.json on the live theme | The app reads config/settings_data.json of the live theme. | Enabled |
| app.extensions() in the admin | Read only while someone has the app open in the admin, then stored with a date. | active. "Not read yet" does not block verification. |
| Beacon from the storefront | The embed calls the app through the app proxy when it runs on a storefront page. | Received in the last 72 hours |
Do:
- Open the storefront once, any page. This sends the beacon. If the store has a password page, enter the password.
- Look at the storefront: the Ship to selector appears in the corner the merchant chose, and the old currency pickers are hidden.
- Go back to the app, click Re-scan store and read the section 3 · Verify.
You should see:
- The banner "Store status: Active" (or "Removed"), with the reasons "Embed enabled on the live theme (settings_data.json)" and "Storefront beacon received recently".
- In the beacon row: when it arrived, the page, and how many legacy elements the embed neutralized.
Stop if: you see "No recent beacon from the storefront" after visiting the storefront. Go to the troubleshooting table (section 5).
Stage 4 · Remove
Goal: delete this app's script tags, one at a time, only after verification. Then clean the theme by hand, with approval.
Do:
- Go to the section 4 · Remove.
- Click Queue script tag removal. The button works only when the store is verified and has script tags. If the store is not verified you see "Removal stays locked until all three signals are green."
- Click Run queue now. Each click processes up to 10 jobs that are due. Read the table: Script tag, Status, Attempts, Note.
- If a job shows retry, click Run queue now again later. The app waits on its own before it tries again (see the table below).
- Read the Theme cleanup checklist. Each line says the file, the line, what it is, and either "Already neutralized by the embed on the storefront." or "Not neutralized yet: activate the embed first."
- Send Template B (section 4) to ask for written approval. Clean only on a duplicated, unpublished theme, then the merchant publishes it.
| Queue status | What it means | What you do |
|---|---|---|
| done | Deleted. "Script tag does not exist" also counts as done. | Nothing. |
| retry | Throttled, 423 locked (usually temporary), a server error or a network error. A 423 waits 15 minutes and doubles each time, up to 6 hours. | Click Run queue now later. Never mark a 423 store as lost. |
| unreachable | 402 frozen store, 404 store closed or app uninstalled, 401/403 token rejected, or a missing scope. | Put it on the unreachable list with its note (see section 5). |
| failed | Any other error. | Escalate (section 7). |
Don't: try to remove anything before the three signals are green. The app refuses it: "The embed is not verified on the live theme yet."
After March 1, 2027, script tags that are left will have no effect. Delete them anyway when you can: they still count as usage.
Stage 5 · Prices by country
Do this stage only if the store fakes a local price with one variant per country (an option such as "Country" with United States, Mexico…). Under Markets, that old variant is converted again, so the buyer sees a wrong price.
Do:
- Go to the section 5 · Prices by country. The first table lists the products with one variant per country: Product, Option, Countries, Stock split by country, Variants.
- If you see "No active market with its own currency for: …", ask the merchant to create that market in Settings → Markets first.
- For each market, read the plan: the market, its catalog, the price list in the market's currency, the number of fixed prices (sent in batches of up to 250) and the audit.
- Read the second table: Variant that stays, Legacy variant, Fixed price to set, Shopify charges in … and Audit.
- Click Migrate prices (market name). Wait for the banner "Prices migrated for …" with its log.
You should see: the Audit column shows "✓ fixed price applied" for every row, and the last log line says the audit matches, for example 1/1 match.
Example from the development store: The Videographer Snowboard in Mexico went from 16,558.00 MXN (885.95 USD converted) to a fixed 15,999.00 MXN. The old "Mexico" variant alone would show 299,006 MXN, because Markets converts the old local price again.
Safe to repeat: the button changes to Run the … migration again. It reuses the catalog and the price list.
Left for the merchant (the kit never deletes variants or products): check open orders, remove the legacy variant, move its stock back to the variant that stays, and, when no legacy variants are left, remove the "Country" option.
Limits: charging in the local currency needs Shopify Payments or Adyen. Rounding works only with Shopify Payments.
Tokens · January 1, 2027
The card Tokens · January 1, 2027 shows one of two banners:
- "This app uses an expiring offline token" (green): ready for January 1, 2027. Nothing to do.
- "Non-expiring offline token" (red): Shopify rejects these from January 1, 2027. This is an app-level change: escalate (section 7).
Report
In the section Report, click Download JSON and Download CSV. The files are named migration-<store>.json and migration-<store>.csv. See section 9.
4. Messages to the merchant
Replace the text in [brackets]. Keep the messages short.
Template A · Ask for the activation click
Subject: One click to keep your currency selector working after March 1, 2027
Hi [name],
Shopify is retiring the old way apps load on storefronts. From March 1, 2027 it stops loading "script tags", and the currency selector on [store] still uses the old method.
We moved the selector to Shopify's new method, called an "app embed". Shopify only lets the store owner switch it on, so we need one click from you:
- Open this link while you are logged in to your Shopify admin: [link from "Activate embed"]
- Make sure "Currency switcher" is on.
- Click Save.
That's all. We check your storefront afterwards and confirm that everything works before we remove anything old.
Thank you,
[your name]
Template B · Ask for approval to clean the theme
Subject: Approval to remove old currency code from your theme
Hi [name],
The new currency selector is running on [store], and it already hides the old code on your storefront. Your theme still has [number] old lines of code from the previous setup:
[paste the "Theme cleanup checklist": file, line, what it is]
We would like to remove them. This is how we work:
- We make a copy of your live theme and clean only the copy.
- We send you a preview link so you can check it.
- You publish the clean copy when you are happy with it. Your current theme stays as a backup.
Please reply "Approved" to this email so we can start. Nothing changes on your live theme without your approval.
Thank you,
[your name]
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The theme editor opens a draft theme. | The official themes/current link has opened the draft theme after a theme switch. The deep link has also failed for all apps for about a day before. | Use Activate embed on the Live row (it uses the theme ID) or the manual path on the live theme. |
| Embed Enabled, but "No recent beacon from the storefront". | Nobody opened the storefront in the last 72 hours. | Open any storefront page, then Re-scan store. |
| Same, after visiting the storefront. | "Report to the app that the embed is running" is off. | Switch it on in the theme editor, Save, visit the storefront again. |
| Same, and the setting is on. | Embeds with target: body are inserted before the first literal </body>. If the theme has </body> inside a comment, the embed lands inside the comment and silently never runs. | Open the browser's developer tools (Elements) on the storefront and check that the <html> tag has data-emk="running". If it is missing, escalate (theme structure). |
| The beacon arrives but there is no Ship to selector. | The store has only one market. | Expected. Ask the merchant to add a second market if they want the selector. |
| The old currency picker is still visible. | "Neutralize legacy currency code left in the theme" is off, or the old picker uses markup the kit doesn't know. | Switch the setting on. If it is on, add the element to the cleanup list. |
| Old and new code both load during the transition. | The old script tag still exists. | Expected until Stage 4. The embed loads itself only once, hides old pickers and keeps the price Shopify rendered. Finish Stage 4. |
| A product shows a huge price in a market, for example 299,006 MXN. | The old per-country variant stores a local price, and Markets converts it again. | Run Stage 5, then the merchant removes the legacy variant. |
| A product shows Sold out. | Its variants have no available stock (common on test stores, and on legacy country variants whose stock was split). | A store setting, outside the kit. The merchant sets the available quantity per variant (Products → the product → Inventory). |
| "Prices by country can't be read yet" | The store hasn't granted read_products, write_products or read_markets. | The merchant approves the app's updated scopes, then Re-scan store. |
Queue: 423 locked (usually temporary) | Shopify locked the store for a while. | The app waits 15 minutes, doubling up to 6 hours. Click Run queue now later. Don't count the store as lost. |
Queue: 402 frozen store | The store is frozen (unpaid balance). Its tags can't be deleted. | Unreachable list, with the cause, for a Shopify support ticket. |
Queue: 404 store closed or app uninstalled | The store is closed or uninstalled the app. | Unreachable list. |
Queue: 401/403 token rejected | The token is invalid, or it is a non-expiring token after January 1, 2027. | The merchant opens the app again to re-authenticate. If it repeats, escalate. |
Queue: missing scope: …write_script_tags… | The store re-authorized without write_script_tags. | The merchant approves the scope again. Never drop this scope before the audit reads zero. |
| Embed state Unreadable. | The app couldn't read config/settings_data.json. | Escalate (section 7). |
| app.extensions() shows "not read yet (needs the merchant in the app)". | Nobody has opened the app in the admin since the change. | Open the app in the store admin. It doesn't block verification. |
6. What the kit can't do
Say these limits early, to the merchant and to your team:
- Switch the embed on for the merchant. The platform requires their click. The kit opens the right theme.
- Rewrite theme code from a public app. There is no
write_themes, and Shopify doesn't grant the exemption to public apps in general. Cleanup is human, on a duplicated theme, with written approval. - Delete script tags on frozen (402), locked or closed stores. They are listed with the cause.
- Run in bulk across stores. Bulk operations are per store, and the queue is per store by design.
- Charge in the local currency without Shopify Payments or Adyen. Rounding works only with Shopify Payments.
- Delete legacy country variants. The merchant decides, after checking orders.
The app shows the same list in the column What this kit can't do.
7. When to escalate to the lead developer
Handle everything else with this runbook. Call the lead developer only when:
- The live proof says "Shopify created the script tag".
- The embed is Enabled, you visited the storefront, the beacon setting is on, and the
<html>tag in the storefront has nodata-emk="running"(developer tools → Elements). - The embed state is Unreadable.
- The token card is red: "Non-expiring offline token".
- A queue job is failed, or a
401/403 token rejectedcomes back after the merchant re-authenticated. - "Migration for … stopped" shows an error you can't match with section 5.
- The theme cleanup needs more than removing the lines in the checklist (custom code around them).
When you escalate, attach the store's JSON report and a screenshot of the section that shows the problem.
8. Per-store checklist
Print one per store.
9. The report and the migration board
Each store gives one report. Merge the CSV files of all stores into one sheet: that is the migration board.
The CSV has one row per finding, with these columns:
| Column | Content |
|---|---|
shop | The store. |
section | script_tag, embed, legacy_marker or verdict. |
theme | The theme name (empty for script tags). |
role | MAIN for the live theme, or the theme's role. |
item | The script tag ID, the embed handle, or file:line of a marker. |
detail | The script source, the activation link, the marker label, or the verdict reasons. |
status | registered, the embed state, the marker type, or the store status (Still registered, Active, Removed). |
Useful views on the board:
- Filter
section = verdictto see the status of every store in one column. - Count
section = script_tagper store to see what is left to delete. - Group the unreachable stores (from the queue notes) into three buckets: frozen (402), locked (423, temporary) and closed (404). Deleting is not the bottleneck: the state of the stores is.
The JSON file has the full report: script tags, each theme with its embed state, markers and last beacon, the verdict, the token and the queue.