<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jack Petersen</title>
    <description>The latest articles on DEV Community by Jack Petersen (@jackpetersencode).</description>
    <link>https://dev.to/jackpetersencode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951446%2F366a9708-d302-4d41-a6aa-07a0e1f8cc02.png</url>
      <title>DEV Community: Jack Petersen</title>
      <link>https://dev.to/jackpetersencode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jackpetersencode"/>
    <language>en</language>
    <item>
      <title>The Shopify Scripts deprecation is 16 days away. Here's a triage playbook.</title>
      <dc:creator>Jack Petersen</dc:creator>
      <pubDate>Sun, 14 Jun 2026 15:12:07 +0000</pubDate>
      <link>https://dev.to/jackpetersencode/the-shopify-scripts-deprecation-is-16-days-away-heres-a-triage-playbook-1jh4</link>
      <guid>https://dev.to/jackpetersencode/the-shopify-scripts-deprecation-is-16-days-away-heres-a-triage-playbook-1jh4</guid>
      <description>&lt;p&gt;If you're a Shopify merchant (or running migrations for one) and your Scripts haven't moved to Functions yet, this post is for you. The June 30, 2026 deadline isn't moving. You have a little over two weeks. Here's how to triage what's left.&lt;/p&gt;

&lt;p&gt;I'm the developer of &lt;a href="https://apps.shopify.com/paylogic" rel="noopener noreferrer"&gt;PayLogic&lt;/a&gt;, a Shopify Functions app that replaces payment-customization Scripts. Most of the migration patterns described below come from reading a steady stream of merchant Scripts while building the parser, and from threads in the Shopify Plus community over the last few months. The framework here is migration-path-agnostic — useful whether you migrate yourself, hire it out, or use an app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I'll mention PayLogic and a free converter I built at the bottom. The tools are interchangeable; the framework is the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is happening, briefly
&lt;/h2&gt;

&lt;p&gt;Shopify Scripts (Ruby, server-side, per-checkout) is being replaced by Shopify Functions (Rust or JS compiled to WASM, deterministic sandbox). Functions have better performance, better security, and stricter resource limits.&lt;/p&gt;

&lt;p&gt;The deprecation has been announced since 2023. Most merchants who used Scripts have already migrated. The ones who haven't typically fall into three buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They forgot.&lt;/strong&gt; Scripts run silently and reliably. Out of sight, out of mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They lost the dev.&lt;/strong&gt; The person who wrote the Scripts left the company a year ago. Nobody on the team can read the Ruby.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They thought the deadline would slip.&lt;/strong&gt; It won't.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're in any of these, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks on July 1
&lt;/h2&gt;

&lt;p&gt;There's no banner. No email. No grace period. At 00:00 UTC on July 1, 2026, in concrete terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Scripts stop executing. Whatever logic they ran at checkout (hide COD, hide express checkout in specific countries, rename payment methods, reorder, surcharge math, custom shipping rules) — none of it runs.&lt;/li&gt;
&lt;li&gt;Your existing payment gateways all reappear in their default state. If you were hiding Cash on Delivery for risky customers, it shows for everyone. If you were hiding express checkout in Saudi Arabia, it appears in Saudi Arabia.&lt;/li&gt;
&lt;li&gt;You won't find out from Shopify. You'll find out from an RTO spike, a customer report, or a fraud-rate alarm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a "things will break loudly" deadline. It's a "things will silently misbehave starting July 1" deadline. The merchants who get blindsided are the ones who assume they'll get a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Inventory your Scripts (today)
&lt;/h2&gt;

&lt;p&gt;Shopify admin → Apps → Scripts editor. List every Script currently running. For each one, capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title and trigger (Payment, Shipping, or Line Item)&lt;/li&gt;
&lt;li&gt;Full Ruby source (copy into a doc)&lt;/li&gt;
&lt;li&gt;Active/inactive status&lt;/li&gt;
&lt;li&gt;Last-modified date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're on Plus but the Scripts editor isn't visible, verify with the Admin API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET &lt;span class="s2"&gt;"https://your-shop.myshopify.com/admin/api/2024-04/scripts.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Shopify-Access-Token: &lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Time budget: 30 minutes. If you've never opened the Scripts editor and you're not sure if you have any, this step is non-optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Categorize each Script
&lt;/h2&gt;

&lt;p&gt;Read each one and put it in one of four buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket A — Trivial translation
&lt;/h3&gt;

&lt;p&gt;Maybe 60% of Scripts I read fall here. Simple conditional logic on cart state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtotal_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100_00&lt;/span&gt;
  &lt;span class="no"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translates cleanly to a single payment-customization Function with one IF condition and one HIDE action. Whether you write the Function yourself or use a rule builder, the result is identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket B — Multiple rules combined
&lt;/h3&gt;

&lt;p&gt;Multiple conditions chained, or multiple actions in one Script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"vip"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtotal_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100_00&lt;/span&gt;
    &lt;span class="no"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still mechanical translation, but you need to be precise about logical operators (AND vs OR), null-safe customer access (some buyers are guests), and operator precedence. A skilled dev or a competent rule builder handles this; a panicked junior dev gets it subtly wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket C — Custom math or behavior Functions don't support
&lt;/h3&gt;

&lt;p&gt;Payment Customization Functions intentionally do less than Scripts did. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No surcharges (you can't ADD an amount via PaymentCustomization)&lt;/li&gt;
&lt;li&gt;No payment-method &lt;em&gt;creation&lt;/em&gt; (hide / rename / reorder existing only)&lt;/li&gt;
&lt;li&gt;No modification of cart line items at the payment step&lt;/li&gt;
&lt;li&gt;No reading shop-level metafields or external data without an explicit input query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your Script does any of these, the Function platform can't replicate it as-is. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A workflow rebuild (often the right call — many surcharge Scripts existed to compensate for behaviors Shopify now supports natively)&lt;/li&gt;
&lt;li&gt;A different Shopify primitive (Cart Validation Function, Discount Function, or Delivery Customization Function depending on what the Script does)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bucket D — Touches data Functions can't access
&lt;/h3&gt;

&lt;p&gt;Functions run in a deterministic sandbox with a strict input query. If your Script reads things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A third-party API at checkout&lt;/li&gt;
&lt;li&gt;Shop metafields not part of the Function input&lt;/li&gt;
&lt;li&gt;Customer order history via the Admin API&lt;/li&gt;
&lt;li&gt;Live shipping-rate quotes from a carrier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you can't replicate it directly. Workarounds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache the relevant data into a metafield via a webhook (Function reads metafield instead of fetching live)&lt;/li&gt;
&lt;li&gt;Move the logic to a webhook-driven order workflow (different latency/UX profile)&lt;/li&gt;
&lt;li&gt;Build a custom backend that hooks into the order lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bucket D is the hardest. If most of your Scripts are here, 16 days might not be enough. Plan accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Pick a migration path per bucket
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Bucket A
&lt;/h3&gt;

&lt;p&gt;Three paths in order of effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A1: Use a payment-customization app.&lt;/strong&gt; Apps with Scripts importers translate Ruby to visual rules automatically. For Bucket A logic, the import is one click. Cost: $2–$10/mo, ongoing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A2: Write the Function yourself.&lt;/strong&gt; &lt;a href="https://shopify.dev/docs/apps/build/functions" rel="noopener noreferrer"&gt;Shopify's Functions docs&lt;/a&gt; walk through it. Install Shopify CLI, scaffold a Function, write the logic in Rust or TypeScript, deploy. Cost: a few hours, zero ongoing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A3: Hire it out.&lt;/strong&gt; Agencies bill $500–$2,000 for a Bucket A migration. Reasonable if you don't have dev capacity.&lt;/p&gt;

&lt;p&gt;For 1–3 simple rules, A2 is fine. For more — or if you'll need to edit rules later without a dev — A1 wins because you get a UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Bucket B
&lt;/h3&gt;

&lt;p&gt;Same three paths, but the cost of A2 doubles (more time, easier to get wrong). I'd skew toward A1 here unless you have a strong Shopify dev on staff.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Bucket C
&lt;/h3&gt;

&lt;p&gt;Workflow rebuild. Talk to a Shopify Plus partner agency. Don't expect to migrate as-is — the platform won't let you, so the rebuild is the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Bucket D
&lt;/h3&gt;

&lt;p&gt;Same. Find an agency that does Functions + custom backend. There aren't many. Reach out today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Test in a draft order (every time)
&lt;/h2&gt;

&lt;p&gt;Before you flip anything live, test in a draft order. This is the step most merchants skip and the one that produces the "wait, it broke" tickets in July.&lt;/p&gt;

&lt;p&gt;The discipline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List the 5 most common checkout scenarios in your store: new customer + low cart, returning customer + high cart, international shipping, COD-eligible region, VIP customer. Whatever maps to your business.&lt;/li&gt;
&lt;li&gt;After implementing the migration, place a draft order for each scenario. Use the Shopify draft-order tool, set the customer, set the cart, place it without charging.&lt;/li&gt;
&lt;li&gt;Verify the checkout behavior matches what the Script used to do.&lt;/li&gt;
&lt;li&gt;If anything's different, fix and re-test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Critical gotcha: if you have OTHER customizations layered on top of your Scripts (checkout.liquid edits, Shopify Flow workflows, third-party payment apps), the post-migration behavior may combine differently than expected. Test the combined behavior, not just the migrated rule in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Schedule the cutover
&lt;/h2&gt;

&lt;p&gt;You want the migrated Function live BEFORE June 30, not on June 30. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy the migrated Function (or app) at least 5 days before the deadline.&lt;/li&gt;
&lt;li&gt;Disable the old Script when the new system is live. Don't run both — Scripts win as long as enabled.&lt;/li&gt;
&lt;li&gt;Test the live behavior in production with a real test order.&lt;/li&gt;
&lt;li&gt;Monitor checkout for 48 hours after cutover. Anomaly? Roll back to Scripts (still works until June 30) and fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you wait until June 29 to flip the switch, you have no rollback window. Don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if you genuinely can't make the deadline
&lt;/h2&gt;

&lt;p&gt;Realistic backup plans:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan 1: Disable Scripts proactively, accept temporary degradation.&lt;/strong&gt; Better to know your COD-hiding logic isn't running than to be surprised on July 1. Disable July 1 morning, manually. Customers see default behavior until the migration ships. You'll know exactly what you're sacrificing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan 2: Ship a minimum viable Function for the highest-stakes Script.&lt;/strong&gt; If your Scripts span 5 use cases but only 1 of them (hiding COD on risky orders, say) actually moves business KPIs, migrate just that one. Let the other 4 lapse. Backfill later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan 3: Use an app for an emergency import.&lt;/strong&gt; Most payment-customization apps have a Scripts importer. Install one. Click import. You'll have a passable migration in under an hour. Might not perfectly replicate every nuance, but it'll restore ~90% of the behavior. Refine later.&lt;/p&gt;

&lt;p&gt;Don't pretend you'll be fine. Pick a backup plan now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final 16-day calendar
&lt;/h2&gt;

&lt;p&gt;If you're reading this with the deadline 16 days out, here's the cadence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Today (weekend)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open the Scripts editor, inventory everything, categorize into four buckets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monday–Tuesday&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pick a migration path per bucket. Start the work for Buckets A and B.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wednesday–Friday&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test in draft orders. Identify gotchas. Fix and re-test.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Following Monday (June 22)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deploy to production. Disable old Scripts. Monitor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;June 22–25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Buffer days for the inevitable surprises.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;June 29&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Final verification. Place a real-money test order, confirm the migrated behavior, breathe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;June 30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No action needed. You're done.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A pitch (skip if not interested)
&lt;/h2&gt;

&lt;p&gt;I built PayLogic as a payment-customization Functions app specifically for the 60–70% of merchants in Buckets A and B above. The Scripts importer pastes Ruby into a textarea, parses it, shows you the visual rules it detects, and one-clicks them live.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://paylogic.dev/scripts-converter" rel="noopener noreferrer"&gt;free standalone in-browser converter at paylogic.dev/scripts-converter&lt;/a&gt; if you want to see what your Scripts translate to without installing anything. No signup, no email capture.&lt;/p&gt;

&lt;p&gt;The app costs $1.99–$9.99/mo depending on the feature set. The cheapest tier covers basic hide/rename/reorder; the most expensive includes a risk-composite rule and a customer-prior-risk rule (the latter reads Shopify's native order-risk signal from a customer's previous orders to auto-hide COD for known-risky returning shoppers — particularly useful for COD-heavy markets fighting return-to-origin losses).&lt;/p&gt;

&lt;p&gt;If PayLogic doesn't fit your use case, the converter is still free and the framework above is migration-path-agnostic. Use whatever ships you by June 30.&lt;/p&gt;

&lt;h2&gt;
  
  
  One last thing
&lt;/h2&gt;

&lt;p&gt;If you're an agency: the merchants you've handled are unlikely to all migrate before the deadline. Some will miss it. Some will be silently broken on July 1 and not notice for weeks. Reach out to them now, even if your last engagement was a year ago. The deadline is your reason to re-establish contact, and most agencies aren't doing it.&lt;/p&gt;

&lt;p&gt;Good luck. The next 16 days matter.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What I learned writing a Shopify Scripts Functions parser</title>
      <dc:creator>Jack Petersen</dc:creator>
      <pubDate>Tue, 02 Jun 2026 18:56:06 +0000</pubDate>
      <link>https://dev.to/jackpetersencode/what-i-learned-writing-a-shopify-scripts-functions-parser-5474</link>
      <guid>https://dev.to/jackpetersencode/what-i-learned-writing-a-shopify-scripts-functions-parser-5474</guid>
      <description>&lt;p&gt;Shopify is killing Scripts on &lt;strong&gt;June 30, 2026.&lt;/strong&gt; That's 28 days from when I'm writing this. After that, every Shopify store running Ruby Scripts to customize their checkout — hide payment methods, rename gateways, apply discount rules, route shipping — will see those Scripts stop executing.&lt;/p&gt;

&lt;p&gt;Most stores still haven't migrated.&lt;/p&gt;

&lt;p&gt;I built PayLogic, a Shopify Functions-based payment-customization app that's now live on the Shopify App Store (disclosure up front — I'm not pretending to be a disinterested observer). To help merchants migrate to it, I needed an importer that could take a Ruby Script as input and produce equivalent rule definitions. I figured: how hard could parsing a small Ruby DSL be?&lt;/p&gt;

&lt;p&gt;Harder than I expected.&lt;/p&gt;

&lt;p&gt;I extracted the parser into a free standalone tool — paste a Script, see what's translatable, what's partial, what won't. Runs entirely in your browser, no signup, no upload. It's at &lt;a href="https://paylogic.dev/scripts-converter" rel="noopener noreferrer"&gt;paylogic.dev/scripts-converter&lt;/a&gt; if you want to play with it.&lt;/p&gt;

&lt;p&gt;But what I want to share here is what I learned by reading a lot of real Shopify Scripts, not the tool. Some of these patterns are useful if you're migrating manually. Some of the surprises are useful if you're ever building anything that integrates with Shopify webhooks. And the war story about an eight-hour debugging session is something I wish someone had written before I had to figure it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deceptively-hard parsing problems
&lt;/h2&gt;

&lt;p&gt;Shopify Scripts are written in a Ruby DSL with a small surface area. From the outside this looks like the easiest possible parsing target — limited syntax, limited semantics, limited shape of business logic. A few regexes and you're done, right?&lt;/p&gt;

&lt;p&gt;Reader, you are not done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two block syntaxes for the same operation
&lt;/h3&gt;

&lt;p&gt;The first thing that breaks a naive parser is Ruby's two block syntaxes. These are semantically identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Single-line brace form&lt;/span&gt;
&lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Multi-line do/end form&lt;/span&gt;
&lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my sample of real merchant Scripts, the multi-line form is way more common. Idiomatic Ruby prefers &lt;code&gt;do...end&lt;/code&gt; for multi-line and &lt;code&gt;{...}&lt;/code&gt; for single-line. So a parser that only handles one form misses the majority of real Scripts.&lt;/p&gt;

&lt;p&gt;The naive approach — regex matching for &lt;code&gt;delete_if[^|]*\|...&lt;/code&gt; — works for the single-line form because the whole pattern is on one line. For the multi-line form, the pattern spans lines, and you have to find the matching &lt;code&gt;end&lt;/code&gt;, which means tracking block depth.&lt;/p&gt;

&lt;p&gt;Which gets harder because...&lt;/p&gt;

&lt;h3&gt;
  
  
  Modifier-if looks just like block-if
&lt;/h3&gt;

&lt;p&gt;Ruby has &lt;em&gt;modifier&lt;/em&gt; &lt;code&gt;if&lt;/code&gt;, which trails an expression and conditionally evaluates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Credit Card"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Bogus Gateway"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one statement. There's no &lt;code&gt;end&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's syntactically near-identical to &lt;em&gt;block&lt;/em&gt; &lt;code&gt;if&lt;/code&gt;, which opens a scope that requires an &lt;code&gt;end&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Bogus Gateway"&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Credit Card"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your block-depth counter sees &lt;code&gt;if&lt;/code&gt; and increments depth, you over-count modifier-ifs and never find the matching &lt;code&gt;end&lt;/code&gt; for your outer block. Distinguishing block-&lt;code&gt;if&lt;/code&gt; from modifier-&lt;code&gt;if&lt;/code&gt; without writing a full Ruby tokenizer required a heuristic: check whether &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;unless&lt;/code&gt; appears at the &lt;em&gt;start&lt;/em&gt; of a stripped line. If so, it's block-opening. If it's mid-line after an expression, it's a modifier.&lt;/p&gt;

&lt;p&gt;This isn't bulletproof — a Script could have &lt;code&gt;if true; do_thing; end&lt;/code&gt; on one line — but in the wild I haven't seen merchant Scripts that complex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compound conditions
&lt;/h3&gt;

&lt;p&gt;Real merchant conditions tend to compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_spent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="no"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cents: &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I split the condition expression on top-level &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt;, then parse each side as its own predicate. For AND/OR mixed cases, I bail with a warning — Functions rules support uniform AND or OR via &lt;code&gt;match_mode&lt;/code&gt;, but parenthesized mixing requires more sophisticated AST work that I haven't done.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;unless&lt;/code&gt; is negation, sometimes invertible
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;unless cond&lt;/code&gt; is &lt;code&gt;if !cond&lt;/code&gt;. For simple predicates with clean inverses, this maps cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original:&lt;/span&gt;
&lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"VIP"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Premium Card"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Becomes:&lt;/span&gt;
&lt;span class="c1"&gt;# "Hide Premium Card when customer is NOT tagged VIP"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For some conditions there's no clean inverse — for example, "cart total ≥ X" inverts to "cart total &amp;lt; X," but boundary handling matters (≤ vs &amp;lt;), and I'd rather warn than approximate. So my parser has a small &lt;code&gt;invertSingleCondition&lt;/code&gt; function that handles tag presence/absence, order-count thresholds, and first-time/returning customer flipping, and warns on anything it can't invert cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three actions, three patterns
&lt;/h3&gt;

&lt;p&gt;The three Script actions for payment customization map to three Ruby patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HIDE: delete_if&lt;/span&gt;
&lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"X"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# RENAME: map with conditional assignment&lt;/span&gt;
&lt;span class="no"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Credit Card"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Bogus Gateway"&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# MOVE: partition into preferred + rest, then concat&lt;/span&gt;
&lt;span class="n"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Shop Pay"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preferred&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detecting which is happening requires recognizing the structure, not just the method name. &lt;code&gt;map&lt;/code&gt; can do other things; in payment Scripts it's specifically the conditional &lt;code&gt;gw.name =&lt;/code&gt; inside the block that means "rename." &lt;code&gt;partition&lt;/code&gt; only counts as "move" if followed by a concat assignment. Etc.&lt;/p&gt;

&lt;p&gt;The good news: in practice, merchants reuse these patterns almost verbatim because they're the canonical Shopify-documented ways to do each operation. So pattern recognition is reasonable. The parser detects all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PCD-redacted-payload trap that took 8 hours
&lt;/h2&gt;

&lt;p&gt;This one isn't about parsing, but it's the most expensive bug I hit while building this app, and it's relevant for anyone building Shopify webhooks.&lt;/p&gt;

&lt;p&gt;PayLogic listens for &lt;code&gt;customers/update&lt;/code&gt; webhooks to maintain a tag cache (so the payment customization Function can read customer tags at checkout without a round-trip). The webhook payload has a &lt;code&gt;tags&lt;/code&gt; field. So I read it, wrote it to a metafield, and shipped.&lt;/p&gt;

&lt;p&gt;The bug: the metafield was getting written with empty tags, even for customers who clearly had tags in Shopify admin.&lt;/p&gt;

&lt;p&gt;Four hours of debugging:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hour 1&lt;/strong&gt;: assumed my webhook handler was buggy. Logged the payload. Tags showed as &lt;code&gt;[]&lt;/code&gt;. So the handler was getting empty tags. Confused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hour 2&lt;/strong&gt;: assumed it was a serialization issue between Prisma and the JSON metafield value. Wasn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hour 3&lt;/strong&gt;: assumed Shopify's webhook delivery was buggy. Tested with a different webhook topic — customer fields were similarly empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hour 4&lt;/strong&gt;: hit on the actual answer. Shopify's &lt;strong&gt;Protected Customer Data (PCD)&lt;/strong&gt; policy redacts customer fields from webhook payloads &lt;strong&gt;independently of whether your app has Admin API access to those same fields.&lt;/strong&gt; Your app might have &lt;code&gt;read_customers&lt;/code&gt; scope and full Admin API access, but webhook payloads strip PCD-protected fields by default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix: ignore &lt;code&gt;payload.tags&lt;/code&gt; entirely. On webhook receipt, take the customer ID from the payload, query Admin GraphQL with that ID, get the real customer record with the real tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeTagCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Right:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`query GetCustomer($id: ID!) { customer(id: $id) { id tags } }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`gid://shopify/Customer/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeTagCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a one-line conceptual fix that took me hours to find because the failure mode — payload looks valid, just empty arrays for the PCD fields — doesn't look like a permissions error. There's no helpful "field redacted" indicator. The fields just look like the customer has no tags.&lt;/p&gt;

&lt;p&gt;If you're building Shopify webhooks: &lt;strong&gt;never trust PCD-protected fields in webhook payloads. Always re-query Admin GraphQL with the entity ID.&lt;/strong&gt; That's the lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  What patterns merchants actually use
&lt;/h2&gt;

&lt;p&gt;Reading real Scripts showed me what merchants actually do, which is narrower than the universe of what Scripts &lt;em&gt;could&lt;/em&gt; do. A handful of patterns cover the bulk:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hide COD over a threshold&lt;/strong&gt; — by far the most common single pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtotal_price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="no"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cents: &lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hide multiple methods for first-time customers&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders_count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Bank Deposit"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Country-gated hides&lt;/strong&gt; (very common in international stores):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shipping_address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;country_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"BR"&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"PayPal"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tag-driven hides&lt;/strong&gt; (B2B / wholesale flows):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"wholesale"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Credit Card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Shop Pay"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Renames for friendlier labels&lt;/strong&gt; (less common, but recurring):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payment_gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Pay on Delivery"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Cash on Delivery"&lt;/span&gt;
  &lt;span class="n"&gt;gw&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These five patterns cover maybe 80% of the payment-customization Scripts I've seen. If you're migrating manually, prioritize these in your Function and don't get distracted optimizing for edge cases that may never appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't translate
&lt;/h2&gt;

&lt;p&gt;Some Script patterns don't have a clean Functions equivalent, and the parser flags them as warnings rather than silently dropping them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateful logic&lt;/strong&gt; like "this discount can only be applied once per customer per session." Functions are stateless — they evaluate the current cart and return operations. Per-customer or per-session state has to live in your app's database, not in the Function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External HTTP calls&lt;/strong&gt; for fraud checks, inventory lookups, etc. Functions can't make outbound HTTP. Pre-compute the data into a metafield via webhooks, then read the metafield from the Function input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tax line modifications.&lt;/strong&gt; Functions can't modify tax lines directly. Discount lines yes, tax lines no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Substring matching on payment names&lt;/strong&gt; (&lt;code&gt;gw.name.downcase.include?("cash")&lt;/code&gt;). Functions match exact names. The parser translates these to approximate exact-name matches with a warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deeply nested compound logic&lt;/strong&gt; that doesn't fit a single AND or OR pass. &lt;code&gt;A &amp;amp;&amp;amp; (B || C)&lt;/code&gt; requires manual rule decomposition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these, the converter outputs a warning explaining what was skipped so the merchant knows what they'd need to rebuild manually. I chose this over silently translating to "close enough" rules. Close-enough silently-modified payment logic is exactly the wrong thing to ship at checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Three things I'd tell my past self:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The deadline (June 30, 2026) is real and tight.&lt;/strong&gt; If you're a merchant or building for merchants, don't underestimate how many stores haven't started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most Scripts ARE mechanically translatable.&lt;/strong&gt; The patterns above cover most of what real merchants do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What doesn't translate is worse than you'd guess.&lt;/strong&gt; Stateful, external-API, and tax-line-modifying Scripts are common enough to matter, and silently-wrong translation is worse than no translation. Be explicit about limits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The converter is at &lt;a href="https://paylogic.dev/scripts-converter" rel="noopener noreferrer"&gt;paylogic.dev/scripts-converter&lt;/a&gt; — no signup, runs entirely client-side. Try it on your own Script if you have one running.&lt;/p&gt;

&lt;p&gt;If you're another Shopify Functions / app dev who's been parsing Scripts (or running into PCD quirks, or anything else in this space) — I'd love to compare notes. The "real Scripts in the wild" data set is small enough right now that anything you've seen is useful.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ruby</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Shopify app for the June 30 Scripts deprecation: V1.3 feature tour, pricing breakdown, and the bug that ate my Friday</title>
      <dc:creator>Jack Petersen</dc:creator>
      <pubDate>Tue, 26 May 2026 14:36:05 +0000</pubDate>
      <link>https://dev.to/jackpetersencode/building-a-shopify-app-for-the-june-30-scripts-deprecation-v13-feature-tour-pricing-breakdown-1nak</link>
      <guid>https://dev.to/jackpetersencode/building-a-shopify-app-for-the-june-30-scripts-deprecation-v13-feature-tour-pricing-breakdown-1nak</guid>
      <description>&lt;p&gt;Shopify Scripts — the Ruby-based payment customization tool that's been around for ~8 years on Shopify Plus — shuts off June 30, 2026. Editing's already disabled since April. Come June 30, any active payment Script silently stops firing and merchant checkouts revert to default.&lt;/p&gt;

&lt;p&gt;Estimates put 2,000-5,000 Plus merchants on Scripts for payment customization specifically. Each one has a hard migration deadline. I built &lt;a href="https://apps.shopify.com/paylogic" rel="noopener noreferrer"&gt;PayLogic&lt;/a&gt; for that.&lt;/p&gt;

&lt;p&gt;V1.3 just shipped and the product is feature-complete for the deadline. Here's the full picture: every feature, what tier it's on, and the reasoning behind the pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1 active install, 1 churned (paid 2 subscription charges, uninstalled within 25 min)&lt;/li&gt;
&lt;li&gt;Total revenue to date: $9.98&lt;/li&gt;
&lt;li&gt;Shipped 4 versions in 5 weeks: V1 → V1.1 → V1.2 → V1.3&lt;/li&gt;
&lt;li&gt;Live on the App Store: &lt;a href="https://apps.shopify.com/paylogic" rel="noopener noreferrer"&gt;apps.shopify.com/paylogic&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Landing page + blog + free in-browser Scripts converter: &lt;a href="https://paylogic.dev" rel="noopener noreferrer"&gt;paylogic.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The feature tour
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Conditions (when does a rule fire?)
&lt;/h3&gt;

&lt;p&gt;PayLogic ships 16 rule templates. The conditions you can build rules around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cart total ≥ or ≤ a threshold&lt;/li&gt;
&lt;li&gt;Item count threshold&lt;/li&gt;
&lt;li&gt;Shipping country (one or many)&lt;/li&gt;
&lt;li&gt;Cart currency (one or many)&lt;/li&gt;
&lt;li&gt;Shipping method title contains a keyword&lt;/li&gt;
&lt;li&gt;First-time customer (no prior orders)&lt;/li&gt;
&lt;li&gt;Returning customer (≥ N prior orders)&lt;/li&gt;
&lt;li&gt;Early customer (≤ N prior orders)&lt;/li&gt;
&lt;li&gt;Big spender (lifetime amount-spent threshold)&lt;/li&gt;
&lt;li&gt;Customer has any of these tags&lt;/li&gt;
&lt;li&gt;Customer is missing all of these tags&lt;/li&gt;
&lt;li&gt;Product in cart has any of these tags&lt;/li&gt;
&lt;li&gt;Risk composite (heuristic combining first-time + cart total + flagged shipping countries — fully merchant-configurable)&lt;/li&gt;
&lt;li&gt;Customer prior risk (uses Shopify's order risk model — more on this below)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Actions (what does the rule do?)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hide a payment method at checkout&lt;/li&gt;
&lt;li&gt;Rename a payment method ("Cash on Delivery" → "Pay at door — $5 handling fee")&lt;/li&gt;
&lt;li&gt;Reorder payment methods (move Shop Pay to position 1 for repeat customers, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Plus
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Multi-condition rules with AND/OR logic&lt;/li&gt;
&lt;li&gt;One-click Shopify Scripts importer — reads your existing Ruby, converts the common patterns to PayLogic rules&lt;/li&gt;
&lt;li&gt;Checkout UI extension that displays a merchant-authored message when a method is hidden&lt;/li&gt;
&lt;li&gt;Auto tag-cache backfill on install (so tag rules work immediately, no manual setup)&lt;/li&gt;
&lt;li&gt;Rule versioning with one-click rollback (last 50 snapshots, Plus tier only)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier breakdown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Starter — $1.99/mo (3 active rules)&lt;/strong&gt;&lt;br&gt;
Cart total, country, currency, item count, shipping method, first-time/returning/early customer rules. Hide/rename/reorder actions. Scripts importer. Email support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard — $4.99/mo (unlimited rules)&lt;/strong&gt;&lt;br&gt;
Everything in Starter, plus custom customer-tag and product-tag rules (read any tag from your Shopify admin, not a hardcoded list), plus the big-spender lifetime-value rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plus — $9.99/mo (unlimited rules + advanced)&lt;/strong&gt;&lt;br&gt;
Everything in Standard, plus the risk composite rule, plus the customer prior-risk rule, plus rule versioning with one-click rollback, plus priority support with a same-business-day reply target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free Development — free&lt;/strong&gt;&lt;br&gt;
Full feature parity for Shopify Partner development stores. Agencies can test PayLogic on every client project at zero cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  The differentiator that earns the Plus upgrade
&lt;/h3&gt;

&lt;p&gt;The customer prior-risk rule exposes Shopify's native order risk signal: "hide Cash on Delivery for customers whose past orders Shopify flagged HIGH or MEDIUM risk."&lt;/p&gt;

&lt;p&gt;Scripts couldn't see this data. Functions can. For COD-heavy markets (MENA, SEA, LATAM) where return-to-origin rates sit at 25-35%, this single rule typically cuts RTO 5-15 percentage points. On a store doing $50k/mo of COD volume, that's $7-9k/year of recovered freight. The app pays for itself in roughly a week.&lt;/p&gt;

&lt;p&gt;This is the rule that exists &lt;em&gt;because&lt;/em&gt; the platform changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why $1.99 when competitors charge $9-25
&lt;/h2&gt;

&lt;p&gt;Most apps in the Shopify payment customization space price at $9-$25/mo with no free tier. I priced PayLogic far below that on purpose.&lt;/p&gt;

&lt;p&gt;The math: most payment Scripts in production do $1-50/month of value for the merchant — they stop the occasional bad order, not transform the business. If the app costs $20/mo, the value gap is thin and most merchants churn within a month.&lt;/p&gt;

&lt;p&gt;I'd rather be the obvious "yes" pick at the entry tier and earn the upgrade to $9.99 Plus via the risk rule. The Free Dev plan is bait for agencies who'd otherwise have to budget the app on every client install.&lt;/p&gt;

&lt;p&gt;Am I leaving money on the table? Maybe. I'll know once I have ~50 paid installs to look at the conversion mix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that ate my Friday
&lt;/h2&gt;

&lt;p&gt;V1.3 (the custom-tag rules) shipped Thursday. I tested it on the dev store. Cash on Delivery wouldn't hide for the tagged customer. Spent ~8 hours debugging this.&lt;/p&gt;

&lt;p&gt;Surface symptom: the rule's condition (&lt;code&gt;customer_has_any_tag: ["myopia-tier-2"]&lt;/code&gt;) evaluated to false even though the customer clearly had the tag in Shopify admin.&lt;/p&gt;

&lt;p&gt;The actual cause was a 4-layer onion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Function reads tags from a metafield (&lt;code&gt;paylogic.tags-cache&lt;/code&gt;), not the live tag field. We cache via webhook.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;customers/update&lt;/code&gt; webhook payload contains a &lt;code&gt;tags&lt;/code&gt; field. I trusted it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify redacts PCD-gated fields in webhook payloads&lt;/strong&gt; independently of Admin API access. The &lt;code&gt;tags&lt;/code&gt; field is redacted even when PCD Level 1 is approved. So &lt;code&gt;payload.tags&lt;/code&gt; was always empty.&lt;/li&gt;
&lt;li&gt;The handler wrote &lt;code&gt;{"tags":[]}&lt;/code&gt; to the cache every time. Worse: every metafield write itself fires a &lt;code&gt;customers/update&lt;/code&gt; webhook → which writes empty again → permanent feedback loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fix: ignore &lt;code&gt;payload.tags&lt;/code&gt; entirely. Use the customer ID from the payload to query Admin GraphQL for the real tags. The Admin API respects PCD properly (returns real tags when granted, errors when not).&lt;/p&gt;

&lt;p&gt;If you're building a Shopify app that touches PCD fields, you almost certainly need to know this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture (for the curious)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Built on Shopify Functions (Rust → WASM), runs at Shopify's edge with sub-50ms p95 latency&lt;/li&gt;
&lt;li&gt;No carrier-service callback hacks — fully Functions-native&lt;/li&gt;
&lt;li&gt;Webhook-cached tag system: the Function reads tags from a metafield kept fresh by &lt;code&gt;customers/update&lt;/code&gt; and &lt;code&gt;products/update&lt;/code&gt; webhook handlers (so tag-based rules match against any tag with no scope or latency penalties)&lt;/li&gt;
&lt;li&gt;Rule config stored in payment customization metafields; full version history kept in Prisma on the app backend&lt;/li&gt;
&lt;li&gt;Free &lt;a href="https://paylogic.dev/scripts-converter/" rel="noopener noreferrer"&gt;Scripts → Rules converter&lt;/a&gt; on paylogic.dev runs the same parser as the in-app importer, so what merchants see in the converter is exactly what they'd get on install&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The product is done for V1. Bottleneck now is distribution, not features.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product Hunt launch in ~10 days (the converter tool is the marquee asset)&lt;/li&gt;
&lt;li&gt;Cold email to 50-100 Scripts-using Plus merchants sourced via BuiltWith&lt;/li&gt;
&lt;li&gt;This post&lt;/li&gt;
&lt;li&gt;Long-form blog content for SEO (already 2 posts up: Scripts migration guide and "5 ways to hide Cash on Delivery compared")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;13-month window until the deadline. The product is ready; distribution is the actual job for the next year.&lt;/p&gt;

&lt;p&gt;If you're on Shopify Plus and use Scripts, the free converter at &lt;a href="https://paylogic.dev/scripts-converter/" rel="noopener noreferrer"&gt;paylogic.dev/scripts-converter/&lt;/a&gt; might save you 10 minutes. Genuinely useful feedback if it misses patterns your scripts use.&lt;/p&gt;

&lt;p&gt;If you're building a Shopify app or pricing indie SaaS, happy to swap notes — pricing strategy, Shopify Functions, distribution tactics, all of it.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>saas</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
