<?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: Dmitry Rechkin</title>
    <description>The latest articles on DEV Community by Dmitry Rechkin (@dmitryrechkin).</description>
    <link>https://dev.to/dmitryrechkin</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%2F1470287%2F9cead2f3-1870-4245-8640-46e20389be3b.png</url>
      <title>DEV Community: Dmitry Rechkin</title>
      <link>https://dev.to/dmitryrechkin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitryrechkin"/>
    <language>en</language>
    <item>
      <title>Configuring WooCommerce Shipping From the Terminal: WP-CLI + Claude Code Skills</title>
      <dc:creator>Dmitry Rechkin</dc:creator>
      <pubDate>Wed, 01 Jul 2026 04:46:01 +0000</pubDate>
      <link>https://dev.to/dmitryrechkin/configuring-woocommerce-shipping-from-the-terminal-wp-cli-claude-code-skills-cae</link>
      <guid>https://dev.to/dmitryrechkin/configuring-woocommerce-shipping-from-the-terminal-wp-cli-claude-code-skills-cae</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; To configure WooCommerce shipping from the command line, use the 1TeamSoftware plugins, which expose their full config and fulfillment surface through WP-CLI: run &lt;code&gt;wp wc-shippo-shipping settings set&lt;/code&gt; to write origin, credentials, and services, &lt;code&gt;wp wc-shippo-shipping validate&lt;/code&gt; to gate a deploy, and &lt;code&gt;wp wc-shippo-shipping rates quote&lt;/code&gt; to prove a live rate comes back. The same commands ship as a Claude Code skill, so an AI agent can run them too.&lt;/p&gt;

&lt;p&gt;This is the terminal how-to; every command is real and uses the Shippo namespace. It assumes you're comfortable with WP-CLI and know your way around WooCommerce shipping zones and methods. You don't need prior carrier-API knowledge.&lt;/p&gt;

&lt;p&gt;Run more than one WooCommerce store and you've met the problem already: shipping setup doesn't survive scale.&lt;/p&gt;

&lt;p&gt;You configure zones, boxes, and carrier credentials by clicking through wp-admin. It works. Then you do it again on staging, then again for the next client, each pass a little different because a settings screen won't let you repeat yourself exactly. The failure mode is the bad part. A customer hits checkout, sees &lt;strong&gt;no shipping options at checkout&lt;/strong&gt;, and leaves. Nothing in the logs says why. You find out from a support email three days later.&lt;/p&gt;

&lt;p&gt;I wanted shipping to behave like the rest of my infrastructure: text I can diff, commit, replay across environments, and gate in CI. That's what the 1TeamSoftware shipping plugins give you. It all runs through WP-CLI, and the same commands are wrapped in a Claude Code skill so an agent can drive them.&lt;/p&gt;

&lt;p&gt;This is the developer tour. Every command below is real, copy-pasteable, and uses the Shippo namespace (&lt;code&gt;wc-shippo-shipping&lt;/code&gt;). Swap the namespace for your carrier and the rest holds. For the complete reference with every flag and output shape, see the full pillar: &lt;a href="https://1teamsoftware.com/woocommerce-shipping-setup-guide-with-wp-cli-and-ai-agents/" rel="noopener noreferrer"&gt;configuring WooCommerce shipping with AI and WP-CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer to watch first?&lt;/strong&gt; Here's the whole setup running end to end from the terminal, then handed to an AI agent. Every command below is in the video.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KyoFazXwU9Y"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  One prerequisite, said plainly
&lt;/h2&gt;

&lt;p&gt;A lot of "official" carrier integrations import orders and print labels but never quote a price at checkout. If your current tool can't return a rate to the cart, no CLI command will make one appear there.&lt;/p&gt;

&lt;p&gt;The plugins here quote &lt;strong&gt;woocommerce live shipping rates&lt;/strong&gt; at cart and checkout, validate addresses, pack boxes, buy labels, and pull tracking. All of it is scriptable. The rest of this assumes you've got that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command shape
&lt;/h2&gt;

&lt;p&gt;Everything follows one pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp &amp;lt;namespace&amp;gt; &amp;lt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;subcommand&amp;gt;] &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;--flags&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace is the plugin slug with &lt;code&gt;-pro&lt;/code&gt; dropped, so Shippo is &lt;code&gt;wc-shippo-shipping&lt;/code&gt; for both the free and PRO editions. Not sure what yours is?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp &lt;span class="nb"&gt;help&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;shipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wc-shippo-shipping    Manage Shippo shipping configuration and fulfillment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing the command list won't hand you: the exact setting keys. Credential fields, service IDs, and box types differ per carrier, so discover them instead of guessing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping settings list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------------------+---------------------+
| key                  | value               |
+----------------------+---------------------+
| testApiToken         | [REDACTED]          |
| liveApiToken         | [REDACTED]          |
| sandbox              | yes                 |
| priceAdjustmentPercent | 1.00              |
| maxShippingRates     | 0                   |
+----------------------+---------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;settings list&lt;/code&gt; is your map. It prints every stored setting with secrets shown as &lt;code&gt;[REDACTED]&lt;/code&gt;, which is also how you find a carrier's credential keys before you write them. Shippo uses &lt;code&gt;testApiToken&lt;/code&gt; and &lt;code&gt;liveApiToken&lt;/code&gt;; other carriers name theirs differently. Read first, write second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with status and validate
&lt;/h2&gt;

&lt;p&gt;Before touching anything, check what the plugin actually sees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping status &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp wc-shippo-shipping validate
wp wc-shippo-shipping zones
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status --format=json&lt;/code&gt; returns something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.4.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Shippo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"live_rates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"San Francisco, CA, US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method_instances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; reports version, carrier, sandbox and debug flags, live-rate state, origin, and method-instance count. Watch what it doesn't report: API connectivity. There's no "connected: true" field, because the only honest connectivity test is asking the carrier for a real quote.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; is the one to put in your pipeline. It runs every config check and reports &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;FAIL&lt;/code&gt;, &lt;code&gt;WARN&lt;/code&gt;, &lt;code&gt;INFO&lt;/code&gt;, or &lt;code&gt;PENDING&lt;/code&gt;, and it &lt;strong&gt;exits non-zero on any FAIL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  Origin address is complete
PASS  API token is set
WARN  No box presets configured; falling back to per-item packing
FAIL  Plugin not attached to zone "Rest of World"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That non-zero exit is what makes it a clean deploy gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping validate &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'all(.[]; .status != "FAIL")'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Shipping config has failures"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; wp wc-shippo-shipping validate&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WARN&lt;/code&gt; doesn't break the build; only &lt;code&gt;FAIL&lt;/code&gt; does. Drop this in your release script and a broken shipping config stops a deploy before customers ever see it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;zones&lt;/code&gt; shows which WooCommerce shipping zones the plugin is attached to, with &lt;code&gt;plugin_active&lt;/code&gt;, &lt;code&gt;instance_id&lt;/code&gt;, and &lt;code&gt;method_id&lt;/code&gt; per zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+-----------------+---------------+-------------+
| zone_id | zone_name       | plugin_active | instance_id |
+---------+-----------------+---------------+-------------+
| 1       | US Domestic     | yes           | 4           |
| 2       | Rest of World   | no            | -           |
+---------+-----------------+---------------+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember this one. It's behind most "no rates" tickets: zone 2 above quotes nothing because the method was never added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading and writing settings
&lt;/h2&gt;

&lt;p&gt;Anything stored is readable and writable from the CLI, credentials and origin included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping settings get origin
wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;origin &lt;span class="s1"&gt;'{"name":"Co","address":"123 Main St","city":"SF","state":"CA","postcode":"94105","country":"US"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Associative settings flatten with bracket notation (&lt;code&gt;origin[city]&lt;/code&gt;, &lt;code&gt;services[usps_priority][enabled]&lt;/code&gt;). Read a single value with &lt;code&gt;settings get &amp;lt;key&amp;gt;&lt;/code&gt;, add &lt;code&gt;--raw&lt;/code&gt; to reveal a secret, write with &lt;code&gt;settings set &amp;lt;key&amp;gt; &amp;lt;value&amp;gt;&lt;/code&gt;, and target one zone with &lt;code&gt;--instance=&amp;lt;id&amp;gt;&lt;/code&gt;. Your origin address now lives in a provisioning script instead of someone's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving rates actually come back
&lt;/h2&gt;

&lt;p&gt;This is the test that matters. A green &lt;code&gt;validate&lt;/code&gt; tells you the config is internally consistent. A real quote tells you the carrier is reachable and the route is actually served.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping rates quote &lt;span class="nt"&gt;--products&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"9405:2"&lt;/span&gt; &lt;span class="nt"&gt;--destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"500 5th Ave, New York, NY 10001, US"&lt;/span&gt;
wp wc-shippo-shipping rates get &lt;span class="nt"&gt;--order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the human log line, the JSON array looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USPS Ground Advantage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8.45"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USPS Priority Mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11.20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rates quote&lt;/code&gt; asks the carrier for a live quote on an ad-hoc product and destination. It needs at minimum a ZIP or postcode plus a country; a full street address sharpens accuracy. One scripting gotcha: it prints a human log line &lt;em&gt;before&lt;/em&gt; the JSON, so when you parse it, extract the array from the first &lt;code&gt;[&lt;/code&gt; to the matching &lt;code&gt;]&lt;/code&gt; rather than leaning on &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt;. &lt;code&gt;rates get&lt;/code&gt; does the same for an existing order by ID.&lt;/p&gt;

&lt;p&gt;If a complete product on a covered route returns nothing, that tells you something real: either a connectivity failure or a route the carrier doesn't serve. Either way you now know, instead of guessing from a blank checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix "no shipping options at checkout"?
&lt;/h2&gt;

&lt;p&gt;When a store shows no rates for some customers but not others, it's almost always one of two things: a zone the plugin isn't attached to, or products with no weight. Here's the workflow I reach for.&lt;/p&gt;

&lt;p&gt;The zone case is sneaky because rates work for &lt;em&gt;you&lt;/em&gt; and not for the buyer in another region. The plugin only returns rates on zones where it's attached as a shipping method, so a missing zone is a silent dead end. You won't see it from your own checkout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping zones
wp &lt;span class="nb"&gt;wc &lt;/span&gt;shipping_zone_method create &lt;span class="nt"&gt;--zone_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="nt"&gt;--method_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wc-shippo-shipping &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;zones&lt;/code&gt; shows each zone's &lt;code&gt;plugin_active&lt;/code&gt;. Where it reads &lt;code&gt;plugin_active: no&lt;/code&gt;, add the method. In zone-based mode, being absent from a zone means no rates there; in global mode it's fine.&lt;/p&gt;

&lt;p&gt;The product-data case is the other half. Rates drop silently when an item has no weight, because the carrier can't quote a parcel that has no size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping products audit &lt;span class="nt"&gt;--detailed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="nt"&gt;--limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completeness_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missing_weight_products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1907&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missing_dimensions_products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1907&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2003&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suspicious_data_products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1559&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;products audit&lt;/code&gt; gives a completeness percentage and flags missing weight, missing dimensions, and suspicious values. The per-product arrays (&lt;code&gt;missing_weight_products&lt;/code&gt;, &lt;code&gt;missing_dimensions_products&lt;/code&gt;, &lt;code&gt;suspicious_data_products&lt;/code&gt;) only appear with &lt;code&gt;--format=json&lt;/code&gt;. Pass &lt;code&gt;--limit=0&lt;/code&gt; so a big catalog isn't capped at 500. Fix the flagged products with &lt;code&gt;wp wc product update&lt;/code&gt;, re-quote, and the rates come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sizing boxes to what you actually ship
&lt;/h2&gt;

&lt;p&gt;Rates look high for a boring reason more often than you'd think: parcels get quoted in oversized boxes nobody matched to the catalog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping products stats &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp wc-shippo-shipping products orders &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2025-12-01 &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp wc-shippo-shipping products fit-box &lt;span class="nt"&gt;--length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12 &lt;span class="nt"&gt;--width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 &lt;span class="nt"&gt;--height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;products stats&lt;/code&gt; returns weight and dimension stats plus a size distribution. &lt;code&gt;products orders&lt;/code&gt; analyzes completed and processing orders: items per order, top destination states, domestic versus international. &lt;code&gt;products fit-box&lt;/code&gt; takes box dimensions (all three required) and reports what percentage of your catalog fits, rotation handled automatically. Together they tell you whether a candidate box set covers what leaves your warehouse before you commit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning rates: markup, floors, and which services show
&lt;/h2&gt;

&lt;p&gt;Same complaint, three flavors: undercharging, too many options at checkout, or a $1.50 Media Mail rate leaking through. All of these adjustment fields live in the free plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;priceAdjustmentPercent 1.15
wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;priceAdjustment 2.00
wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;minRateCost 5.00
wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;maxShippingRates 3
wp wc-shippo-shipping services &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp wc-shippo-shipping settings &lt;span class="nb"&gt;set &lt;/span&gt;services &lt;span class="s1"&gt;'{"usps_ground_advantage":{"enabled":true,"name":"USPS Ground Advantage"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch &lt;code&gt;priceAdjustmentPercent&lt;/code&gt;. It's a multiplier, so &lt;code&gt;1.15&lt;/code&gt; adds 15%. Set it to &lt;code&gt;15&lt;/code&gt; and you multiply every rate fifteenfold, which makes for a funny bug report after the fact. Adjustments apply in order: carrier rate, then percent, then fixed, then floor and ceiling, then the max-count cap. To control which services quote at all, discover real IDs with &lt;code&gt;services&lt;/code&gt; first and write them with &lt;code&gt;settings set services&lt;/code&gt;. Never invent service IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PRO half: labels, tracking, manifests
&lt;/h2&gt;

&lt;p&gt;Once a license is active, fulfillment is on the CLI too, and it behaves the same whether a person, an agent, or a cron job runs it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp wc-shippo-shipping labels purchase &lt;span class="nt"&gt;--orders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1240,1241,1242
wp wc-shippo-shipping labels pdf &lt;span class="nt"&gt;--orders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1240,1241,1242 &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/labels/2026-06-11.pdf
wp wc-shippo-shipping track abc123 &lt;span class="nt"&gt;--order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1240 &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp wc-shippo-shipping manifest create &lt;span class="nt"&gt;--shipments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123,def456 &lt;span class="nt"&gt;--ship-date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-06-11
wp wc-shippo-shipping license activate YOUR-KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;labels purchase&lt;/code&gt; buys a batch and auto-creates shipments if they're missing, but it logs label &lt;em&gt;counts&lt;/em&gt;, not tracking numbers, so pull those from &lt;code&gt;track&lt;/code&gt; or &lt;code&gt;shipments get&lt;/code&gt;. &lt;code&gt;labels pdf&lt;/code&gt; merges many orders into one print-ready file and errors if the target already exists, so dated filenames are the safe move. &lt;code&gt;manifest create&lt;/code&gt; builds a carrier SCAN form from shipment IDs. And &lt;code&gt;license activate&lt;/code&gt; takes the key as a positional argument, not &lt;code&gt;--key=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a nightly fulfillment run is a five-line script: collect the day's processing order IDs, buy labels in one batch, merge to a dated PDF for the print station.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing it to Claude Code
&lt;/h2&gt;

&lt;p&gt;The commands above are stable enough to script directly. There's a second mode too: install a skill and let an AI agent drive the same CLI from plain language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/1TeamSoftware/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; 1teamsoftware-wc-shipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood the &lt;a href="https://github.com/1TeamSoftware/skills" rel="noopener noreferrer"&gt;skill is a small library of playbooks&lt;/a&gt;. Ask it to fix box coverage and it reaches for the box-sizing routine instead of guessing at flags. It runs the exact same WP-CLI a human would, and it pauses to ask for secrets rather than inventing them. No hidden API layer, no magic. The free plugin ships 6 skills; PRO adds 4.&lt;/p&gt;

&lt;p&gt;In practice you describe the job with real constraints and let it work one step at a time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Customers in some regions say no shipping options appear. Make the plugin active for both my US domestic zone and Rest of World, then show me the zone coverage table so I can confirm it's attached where it needs to be."&lt;/p&gt;

&lt;p&gt;"Set up Shippo from scratch. Sandbox key is &lt;code&gt;shippo_test_abc123&lt;/code&gt;, we ship from 215 Clayton St, San Francisco 94117. Audit my catalog for missing weight or dimensions, recommend a starter box set from what I actually sell, and prove rates come back with a real quote. Ask one question at a time and don't touch my live key yet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The security model helps anyone managing a fleet: hand the agent a sandbox key during setup, rotate to the live key yourself, and it never needs to see the real one. Secrets stay redacted in CLI output, and you can store credentials in &lt;code&gt;wp-config.php&lt;/code&gt; constants so they never touch the database at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which carriers does this work with?
&lt;/h2&gt;

&lt;p&gt;The same engine, settings model, and command set run across eight carrier plugins: Shippo, EasyPost, FedEx, ShipStation, ShipEngine, Shipmondo, ChitChats, and Stallion Express. Only the namespace and a few setting keys change. Run &lt;code&gt;wp help | grep shipping&lt;/code&gt; to find the namespace, then &lt;code&gt;settings list&lt;/code&gt; to discover the keys.&lt;/p&gt;

&lt;p&gt;One catch worth knowing: the current &lt;a href="https://1teamsoftware.com/product/woocommerce-shipstation-shipping-pro/" rel="noopener noreferrer"&gt;ShipStation plugin&lt;/a&gt; runs on the carrier's API v2, so its namespace is &lt;code&gt;wc-shipstation-shipping-v2&lt;/code&gt;, not the legacy &lt;code&gt;wc-shipstation-shipping&lt;/code&gt;. When in doubt, trust what &lt;code&gt;wp help&lt;/code&gt; prints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs: what this doesn't do
&lt;/h2&gt;

&lt;p&gt;The CLI isn't free of edges, so here they are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free vs PRO is gated.&lt;/strong&gt; Configuration, diagnostics, and live rate quotes work in the free plugin. Labels, tracking, and manifests need an active PRO license, and those commands fail without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent can't invent secrets.&lt;/strong&gt; Hand Claude Code a store with no API token and it stops to ask. By design it never fabricates a credential or a service ID, so first-run setup still needs you in the loop for keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's a CLI learning curve.&lt;/strong&gt; If you've never touched WP-CLI, the first hour is slower than clicking through wp-admin. The payoff shows up on store two, not store one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No connectivity check shortcut.&lt;/strong&gt; &lt;code&gt;validate&lt;/code&gt; confirms the config is internally consistent, not that the carrier is reachable. The only real proof is a &lt;code&gt;rates quote&lt;/code&gt;, which costs an API round-trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;priceAdjustmentPercent&lt;/code&gt; is a multiplier, not a percent field.&lt;/strong&gt; &lt;code&gt;1.15&lt;/code&gt; means +15%. Pass &lt;code&gt;15&lt;/code&gt; and you've multiplied every rate fifteenfold. Easy to fat-finger.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why bother, if a settings screen works?
&lt;/h2&gt;

&lt;p&gt;For one store you set up once, it doesn't. The UI is fine.&lt;/p&gt;

&lt;p&gt;The CLI earns its keep the moment shipping becomes something you &lt;em&gt;operate&lt;/em&gt;: multiple stores, staging-to-production parity, agency work that repeats per client, audits you can re-run on demand, a deploy gate that blocks releases on a broken config, and increasingly a workflow you can hand to an agent. That's the same engine already running on thousands of WooCommerce stores, so this is a well-worn path, not an experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The free &lt;a href="https://1teamsoftware.com/product/woocommerce-shippo-shipping-pro/" rel="noopener noreferrer"&gt;Shippo plugin&lt;/a&gt; covers configuration and live rate quotes; PRO adds labels, tracking, and manifests. Install the skill, point Claude Code at a staging store, and run a &lt;code&gt;validate&lt;/code&gt; plus a &lt;code&gt;rates quote&lt;/code&gt; to watch the whole loop work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/1TeamSoftware/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; 1teamsoftware-wc-shipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever debugged a blank checkout at 11pm, shipping you can diff and replay is worth the ten minutes it takes to set up. The full command reference and AI-agent walkthrough live in the &lt;a href="https://1teamsoftware.com/woocommerce-shipping-setup-guide-with-wp-cli-and-ai-agents/" rel="noopener noreferrer"&gt;WooCommerce shipping WP-CLI and AI pillar&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://1teamsoftware.com/woocommerce-shipping-setup-guide-with-wp-cli-and-ai-agents/" rel="noopener noreferrer"&gt;Configuring WooCommerce shipping with AI and WP-CLI&lt;/a&gt;: the full command reference with every flag and output shape&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/1TeamSoftware/skills" rel="noopener noreferrer"&gt;1TeamSoftware agent skills repo&lt;/a&gt;: read the playbooks before you install one&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://1teamsoftware.com/product/woocommerce-shippo-shipping-pro/" rel="noopener noreferrer"&gt;Free Shippo plugin on the product page&lt;/a&gt;: configuration plus live rate quotes at no cost&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://make.wordpress.org/cli/handbook/" rel="noopener noreferrer"&gt;WP-CLI handbook&lt;/a&gt;: if the &lt;code&gt;wp&lt;/code&gt; command itself is new to you&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://woocommerce.com/document/setting-up-shipping-zones/" rel="noopener noreferrer"&gt;WooCommerce shipping zones documentation&lt;/a&gt;: the zone model these commands attach to&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>woocommerce</category>
      <category>wordpress</category>
      <category>wpcli</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Stopped Clicking Around wp-admin and Treated WordPress Media Offload Like Infrastructure</title>
      <dc:creator>Dmitry Rechkin</dc:creator>
      <pubDate>Sun, 08 Mar 2026 01:20:28 +0000</pubDate>
      <link>https://dev.to/dmitryrechkin/i-stopped-clicking-around-wp-admin-and-treated-wordpress-media-offload-like-infrastructure-1kae</link>
      <guid>https://dev.to/dmitryrechkin/i-stopped-clicking-around-wp-admin-and-treated-wordpress-media-offload-like-infrastructure-1kae</guid>
      <description>&lt;h1&gt;
  
  
  I Stopped Clicking Around wp-admin and Treated WordPress Media Offload Like Infrastructure
&lt;/h1&gt;

&lt;p&gt;WordPress media libraries have a habit of turning into infrastructure problems in slow motion. At first it is just uploads. Then it is product galleries, thumbnails, originals, PDFs, course files, and downloads piling up on the same server that is supposed to stay lean.&lt;/p&gt;

&lt;p&gt;That arrangement does not age well. Disks fill up, backups get bloated, restore times get longer, and moving a site between environments becomes more painful than anyone expected. The media library quietly becomes one of the least portable parts of the stack.&lt;/p&gt;

&lt;p&gt;The first fix is straightforward: stop keeping all of that media on the WordPress server. Offloading uploads to object storage such as Cloudflare R2, Amazon S3, or Google Cloud Storage takes that weight off the app layer and makes the stack easier to scale.&lt;/p&gt;

&lt;p&gt;But offloading media creates a second problem. Somebody still has to configure the provider, test the account, migrate existing files, verify URL rewriting, and repeat the setup without drift across environments. That is where a settings page starts to feel pretty flimsy.&lt;/p&gt;

&lt;p&gt;If media offload matters to your stack, it needs a repeatable CLI workflow with &lt;code&gt;wp&lt;/code&gt; and &lt;code&gt;wp cloudsync&lt;/code&gt;, explicit verification steps, and clear boundaries for where AI is allowed to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat WordPress media offload as infrastructure, not as a page full of one-time clicks.&lt;/li&gt;
&lt;li&gt;Use WP-CLI as the execution layer.&lt;/li&gt;
&lt;li&gt;Use agent skills for repeatability, not for "magic."&lt;/li&gt;
&lt;li&gt;Verify what your installed plugin edition actually supports with &lt;code&gt;wp help cloudsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Be honest about what you have tested locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The thing that kept bugging me
&lt;/h2&gt;

&lt;p&gt;Most WordPress automation discussions get weirdly abstract, weirdly fast.&lt;/p&gt;

&lt;p&gt;People jump straight to "AI agents managing infrastructure" without first answering a much more boring question: is the workflow itself any good?&lt;/p&gt;

&lt;p&gt;If the setup depends on memory, screenshots, and whoever configured production six months ago, an agent is not going to save you. It will just fail faster and in a more exciting way.&lt;/p&gt;

&lt;p&gt;The useful version is much smaller:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down the exact CLI steps.&lt;/li&gt;
&lt;li&gt;Decide what is allowed to run.&lt;/li&gt;
&lt;li&gt;Add verification points.&lt;/li&gt;
&lt;li&gt;Let the agent execute the boring parts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is it. No mystery. No hand waving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why media offload becomes an infrastructure concern
&lt;/h2&gt;

&lt;p&gt;Once uploads stay on the app server, you get the usual headaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;disk usage grows in places you were hoping would stay disposable&lt;/li&gt;
&lt;li&gt;backups get bigger and slower&lt;/li&gt;
&lt;li&gt;multi-environment parity gets sloppy&lt;/li&gt;
&lt;li&gt;stateless deployments stop being truly stateless&lt;/li&gt;
&lt;li&gt;migration work gets riskier than it needs to be&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving media to object storage helps. Doing it repeatably is the real win.&lt;/p&gt;

&lt;p&gt;That is why WP-CLI matters here. A GUI is fine for exploration. It is not a serious operating model for repeated rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI actually fits
&lt;/h2&gt;

&lt;p&gt;A plugin with meaningful CLI coverage gives an agent something it can use safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;known commands&lt;/li&gt;
&lt;li&gt;known flags&lt;/li&gt;
&lt;li&gt;visible output&lt;/li&gt;
&lt;li&gt;a place to stop and verify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudSync Master exposes this through &lt;code&gt;wp cloudsync&lt;/code&gt;, but the exact command set depends on whether you are running the free plugin or Pro. Check &lt;code&gt;wp help cloudsync&lt;/code&gt; on the target site first. Do not write a giant automation layer around commands you have not even confirmed exist.&lt;/p&gt;

&lt;p&gt;If your team uses reusable agent instructions, 1TeamSoftware already publishes them here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/1TeamSoftware/skills/tree/main/1teamsoftware" rel="noopener noreferrer"&gt;1TeamSoftware skills repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/1TeamSoftware/skills/tree/main/1teamsoftware/wp-cloudsync-master" rel="noopener noreferrer"&gt;wp-cloudsync-master skill&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/1TeamSoftware/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; 1teamsoftware/wp-cloudsync-master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the agent plugin-specific instructions. It does not replace judgment. It just reduces guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was verified locally
&lt;/h2&gt;

&lt;p&gt;On a LocalWP site, I was able to get WP-CLI working against the correct PHP runtime and MySQL socket, then verify these free CloudSync commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync status
wp cloudsync accounts list
wp cloudsync &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those worked locally.&lt;/p&gt;

&lt;p&gt;The broader Pro command surface was &lt;strong&gt;not&lt;/strong&gt; fully validated in that environment. During CLI testing, the Pro plugin hit a fatal because of a missing &lt;code&gt;NullTaskScheduler&lt;/code&gt; class. So the larger command blocks below should be read as Pro-oriented workflow examples unless they have been verified on the target site.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical R2 workflow
&lt;/h2&gt;

&lt;p&gt;Cloudflare R2 comes up often for WordPress media for an obvious reason: egress.&lt;/p&gt;

&lt;p&gt;If you serve a lot of images, egress is often the part that gets expensive in a hurry. R2 changes that conversation. It does not make storage free, and it does not remove operational mistakes, but it does change the cost model enough that teams keep revisiting it.&lt;/p&gt;

&lt;p&gt;Official pricing reference: &lt;a href="https://developers.cloudflare.com/r2/pricing/" rel="noopener noreferrer"&gt;Cloudflare R2 Pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a Pro-oriented example flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add and activate the R2 account
&lt;/h3&gt;

&lt;p&gt;Before you run this, replace the placeholders with real values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;R2_ACCESS_KEY&lt;/code&gt;: your R2 access key ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R2_SECRET_KEY&lt;/code&gt;: the matching secret&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R2_ACCOUNT_ID&lt;/code&gt;: your Cloudflare account ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You usually get them from the Cloudflare dashboard while creating the R2 API token and bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync accounts add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cloudflare_r2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Production R2"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-media-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--access-key-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$R2_ACCESS_KEY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-access-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$R2_SECRET_KEY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&lt;span class="nv"&gt;$R2_ACCOUNT_ID&lt;/span&gt;.r2.cloudflarestorage.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--custom-domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;media.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--activate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Test it before you do anything expensive
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync accounts &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where a lot of bad migrations should stop. Wrong endpoint, wrong key pair, wrong bucket policy, clock skew: better to find it here than halfway through a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Apply baseline settings
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync settings &lt;span class="nb"&gt;set &lt;/span&gt;createObjectOnFileUpload &lt;span class="nb"&gt;yes
&lt;/span&gt;wp cloudsync settings &lt;span class="nb"&gt;set &lt;/span&gt;rewriteFileUrlWithObjectUrl &lt;span class="nb"&gt;yes
&lt;/span&gt;wp cloudsync settings &lt;span class="nb"&gt;set &lt;/span&gt;uploadBatchSize 25
wp cloudsync settings &lt;span class="nb"&gt;set &lt;/span&gt;uploadConcurrency 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would start conservatively. Teams love turning concurrency up too early, then acting surprised when the host or provider becomes the bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Sync existing media
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--batch-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Stop and look at the system
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync status &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
wp cloudsync queue status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your automation cannot pause and make someone look at state, it is not mature enough yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhsa9ecxgafqwql1xsuny.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhsa9ecxgafqwql1xsuny.webp" alt="Migration workflow" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration is where people get nervous
&lt;/h2&gt;

&lt;p&gt;Fair enough. They should.&lt;/p&gt;

&lt;p&gt;Most teams are not setting this up from scratch. They are moving from an existing plugin and trying not to break URLs, image rendering, or download paths along the way.&lt;/p&gt;

&lt;p&gt;For teams moving from WP Offload Media, a Pro migration flow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync migrate detect
wp cloudsync migrate import &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp-offload-media
wp cloudsync accounts list
wp cloudsync accounts &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;imported-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the part I would scrutinize hardest during evaluation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can it import metadata from the current plugin?&lt;/li&gt;
&lt;li&gt;can it verify the imported account cleanly?&lt;/li&gt;
&lt;li&gt;can you keep serving media safely during the transition?&lt;/li&gt;
&lt;li&gt;can you move configuration out of the admin UI after cutover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A direct comparison is here: &lt;a href="https://1teamsoftware.com/cloudsync-master-vs-wp-offload-media/" rel="noopener noreferrer"&gt;CloudSync Master vs WP Offload Media&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current provider support note
&lt;/h2&gt;

&lt;p&gt;As of March 5, 2026, the WordPress.org listing for WP Offload Media Lite includes Amazon S3, DigitalOcean Spaces, and Google Cloud Storage, but not Cloudflare R2. For teams that want to use R2, that difference matters.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://wordpress.org/plugins/amazon-s3-and-cloudfront/" rel="noopener noreferrer"&gt;WP Offload Media Lite on WordPress.org&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardening step people skip
&lt;/h2&gt;

&lt;p&gt;One of the better habits in infrastructure work is moving from "it works in the dashboard" to "this is managed configuration now."&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;configure the account&lt;/li&gt;
&lt;li&gt;validate it&lt;/li&gt;
&lt;li&gt;export the hardened configuration&lt;/li&gt;
&lt;li&gt;move the source of truth to &lt;code&gt;wp-config.php&lt;/code&gt; or environment variables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Pro, that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cloudsync config generate &lt;span class="nt"&gt;--account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1mf4ydk0jwga5dyhzbou.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1mf4ydk0jwga5dyhzbou.webp" alt="Configuration hardening" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not the flashiest part of the workflow, but it is the bit that makes audits, environment parity, and incident response less annoying later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few mistakes I keep seeing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Treating prompts as process
&lt;/h3&gt;

&lt;p&gt;A good prompt is not the asset. The asset is a repeatable workflow with guardrails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling out to production first
&lt;/h3&gt;

&lt;p&gt;No. Use staging with representative media first. Always.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hiding uncertainty
&lt;/h3&gt;

&lt;p&gt;If you have not tested the command on a real site, say that. Developers can handle uncertainty. What they hate is fake certainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaving rollback vague
&lt;/h3&gt;

&lt;p&gt;"We can just revert it" is not a rollback plan.&lt;/p&gt;

&lt;p&gt;Write down the exact rollback steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;switch active account back&lt;/li&gt;
&lt;li&gt;disable URL rewriting if needed&lt;/li&gt;
&lt;li&gt;keep fallback behavior intact until samples are clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple evaluation checklist
&lt;/h2&gt;

&lt;p&gt;If I were choosing a WordPress media offload plugin today, I would ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can I operate it through CLI instead of through screenshots and memory?&lt;/li&gt;
&lt;li&gt;Can I test an account before migration?&lt;/li&gt;
&lt;li&gt;Can I migrate from WP Offload Media or another existing plugin?&lt;/li&gt;
&lt;li&gt;Can I move config into &lt;code&gt;wp-config.php&lt;/code&gt; or env vars?&lt;/li&gt;
&lt;li&gt;Can I start with the free version before committing to Pro?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one matters more than people admit.&lt;/p&gt;

&lt;p&gt;You can start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wordpress.org/plugins/wp-cloudsync-master/" rel="noopener noreferrer"&gt;CloudSync Master Free on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://1teamsoftware.com/product/cloudsync-master-pro/" rel="noopener noreferrer"&gt;CloudSync Master Pro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://1teamsoftware.com/ai-agent-offload-configuration/" rel="noopener noreferrer"&gt;Automating WordPress Media Offloading with AI Agents and WP-CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://1teamsoftware.com/wordpress-cloudflare-r2-media-offload/" rel="noopener noreferrer"&gt;Cloudflare R2 WordPress media offload guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://1teamsoftware.com/cloudsync-master-vs-wp-offload-media/" rel="noopener noreferrer"&gt;CloudSync Master vs WP Offload Media&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>devops</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The 67MB Problem: Building a Lightweight WordPress Media Offload Alternative</title>
      <dc:creator>Dmitry Rechkin</dc:creator>
      <pubDate>Tue, 24 Feb 2026 20:52:11 +0000</pubDate>
      <link>https://dev.to/dmitryrechkin/the-67mb-problem-building-a-lightweight-wordpress-media-offload-alternative-4cbh</link>
      <guid>https://dev.to/dmitryrechkin/the-67mb-problem-building-a-lightweight-wordpress-media-offload-alternative-4cbh</guid>
      <description>&lt;p&gt;We had unused Google Cloud credits sitting around, so I threw WordPress onto &lt;strong&gt;Google Cloud Run&lt;/strong&gt; as a serverless experiment. It went fine until I hit the media library.&lt;/p&gt;

&lt;p&gt;That roadblock led me down a rabbit hole: trying to offload media from WordPress to cloud storage usually means installing heavy, bloated SDKs. Here’s how that discovery led us to build a different kind of WordPress media offload plugin.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you run a CMS in a stateless environment, the biggest headache is always the same: where do the uploads go?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F248j8jx92gbrqiyz7w3m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F248j8jx92gbrqiyz7w3m.webp" alt="Horizontal Scaling Diagram" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To horizontally scale compute instances, your infrastructure has to be stateless. Every image or PDF uploaded to the WordPress media library needs to go straight to an object store like Google Cloud Storage (GCS). Otherwise, your containers drop out of sync, and images start breaking the second a new instance spins up.&lt;/p&gt;

&lt;p&gt;I assumed finding a plugin to connect WordPress to Google Cloud Storage would take five minutes. &lt;/p&gt;

&lt;p&gt;It didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Discovering SDK Bloat
&lt;/h2&gt;

&lt;p&gt;I started testing existing WordPress offload media plugins. Since I was targeting GCS, my first thought was to grab one built on the official Google Cloud PHP SDK.&lt;/p&gt;

&lt;p&gt;Then I checked the deployment sizes.&lt;/p&gt;

&lt;p&gt;One of the most popular offload plugins weighed &lt;strong&gt;67MB&lt;/strong&gt; installed.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;entire WordPress core&lt;/em&gt; is about 73MB. A single offload plugin was nearly the size of the CMS itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2zq4eepy0asmdcm8ilc1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2zq4eepy0asmdcm8ilc1.webp" alt="67MB SDK Comparison" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why does a plugin that just moves images from Point A to Point B need to rival the size of WordPress? Two words: SDK Bloat.&lt;/p&gt;

&lt;p&gt;Bundling an official SDK (like the AWS SDK or Google Cloud PHP Client) gets you the methods to upload and delete files. But it also drags in tens of megabytes of boilerplate, dependencies, and API definitions for cloud services you are never going to touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this breaks serverless WordPress:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On a standard $5/month VPS, a 67MB plugin slows down WP-Admin a bit. Annoying, but manageable.&lt;/li&gt;
&lt;li&gt;In serverless environments, deployment size dictates performance. Cold start times and deployment speeds depend on keeping the codebase small.&lt;/li&gt;
&lt;li&gt;I also experimented with running WordPress on Vercel (see my &lt;a href="https://github.com/dmitryrechkin/vercel-wordpress" rel="noopener noreferrer"&gt;vercel-wordpress repo&lt;/a&gt;), where serverless functions have a 250MB size limit. WordPress is ~73MB, WooCommerce adds another ~51MB, and one 67MB offload plugin puts you at 191MB — leaving barely 60MB for your theme and every other plugin combined.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shipping a 67MB vendor folder just to upload images feels like bad architecture. It increases cold-start latency, slows down deployment pipelines, and adds thousands of files to your security surface area.&lt;/p&gt;

&lt;p&gt;I wanted a lightweight WordPress cloud storage plugin that could offload media without dragging down performance. Since I couldn't find one that was actually lean, I pulled in my team and we decided to build it ourselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Dropping the SDK Entirely
&lt;/h2&gt;

&lt;p&gt;Think about what a WordPress media offload plugin actually needs to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate.&lt;/li&gt;
&lt;li&gt;PUT an object.&lt;/li&gt;
&lt;li&gt;DELETE an object.&lt;/li&gt;
&lt;li&gt;Generate a signed URL for private files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We don't need a unified SDK for that. We just need to make authenticated REST API calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generic SDK clients were replaced with focused Guzzle requests&lt;/span&gt;
&lt;span class="nv"&gt;$client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\GuzzleHttp\Client&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cloud_provider_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$authentication_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'body'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file_stream&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use Guzzle for the underlying HTTP requests.&lt;/li&gt;
&lt;li&gt;Uploads happen asynchronously in the background. This keeps the upload process from blocking the main WordPress thread. Nobody wants a slow WP-Admin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wrote our own authentication wrappers (handling AWS V4 signatures and Google OAuth) and talked directly to the cloud REST APIs. This let us throw out the bloated SDKs entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Engineering CloudSync Master
&lt;/h2&gt;

&lt;p&gt;That realization turned into &lt;strong&gt;CloudSync Master&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Focusing purely on the necessary API calls brought the installed plugin size down to &lt;strong&gt;under 2.5MB&lt;/strong&gt; (the PRO build compiles to 1.5MB).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwtu16z7m3e5xrorb2mh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwtu16z7m3e5xrorb2mh.png" alt="CloudSync Master General Settings Dashboard" width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s roughly 30x smaller than the 67MB behemoth we started with. The codebase is lean, 95% type-safe, and it doesn’t drag down the site’s admin performance.&lt;/p&gt;

&lt;p&gt;The project started as a quick way to run WordPress on Google Cloud Run using free credits. But halfway through, I paused the migration. Storage itself is cheap, but serving those files (bandwidth egress) adds up fast.&lt;/p&gt;

&lt;p&gt;We pivoted to &lt;strong&gt;Cloudflare R2&lt;/strong&gt;. The reason is simple: R2 charges &lt;strong&gt;zero egress fees&lt;/strong&gt; and comes with Cloudflare's global CDN built in. For a media-heavy WordPress site, that's a significant cost difference.&lt;/p&gt;

&lt;p&gt;Switching providers created a new problem: moving existing media from GCS to R2. Downloading gigabytes of files to small server instances just to re-upload them would instantly blow out our local disk space and crash the container.&lt;/p&gt;

&lt;p&gt;Instead, we built a cross-provider migration engine directly into the plugin. It streams files from the old provider into memory, pushes them to the new provider, and rewrites the database URLs, all in the background. Files never touch the local disk.&lt;/p&gt;

&lt;p&gt;This lightweight, API-first approach scaled better than we expected. CloudSync Master now supports 10 providers—Amazon S3, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, Wasabi, and others—inside that same 2.5MB footprint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5a603s5bg9cvodi97pxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5a603s5bg9cvodi97pxt.png" alt="Setup Wizard Providers" width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: The Sync Bottleneck (Why Background Queues Matter)
&lt;/h2&gt;

&lt;p&gt;When you're migrating a site with 10,000 existing images, you start to notice how WordPress handles file uploads. &lt;/p&gt;

&lt;p&gt;By default, the process is painfully synchronous. You upload a file. The server processes it, generates 5-7 different thumbnail sizes, and then (if you're using an offload plugin) pushes every single one of those files to the cloud over an external HTTP request. &lt;/p&gt;

&lt;p&gt;If you do this synchronously, the main PHP thread stays blocked the entire time. The WordPress dashboard hangs. If you try to bulk-upload a gallery, PHP hits its &lt;code&gt;max_execution_time&lt;/code&gt; limit, and your server throws a 504 Gateway Timeout.&lt;/p&gt;

&lt;p&gt;To fix this, we completely decoupled the upload process.&lt;/p&gt;

&lt;p&gt;Instead of pushing to the cloud immediately, CloudSync Master queues the job. It uses background scheduled tasks (cron) to validate and push items asynchronously. You can drop 500 images into the media library, close the browser tab, and go get coffee. &lt;/p&gt;

&lt;h3&gt;
  
  
  Giving Users Control Under the Hood
&lt;/h3&gt;

&lt;p&gt;We learned quickly that async "black boxes" frustrate users. If something fails, you need to know what and why. So we built a real-time queue dashboard into the plugin. If a file fails because of a network blip or a provider timeout, you click retry on that item. No restarting the whole batch.&lt;/p&gt;

&lt;p&gt;We also introduced a &lt;strong&gt;Concurrency&lt;/strong&gt; slider. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By default, the plugin processes 5 items simultaneously.&lt;/li&gt;
&lt;li&gt;If you're on a capable server and need to move a massive library fast, you can crank the concurrency up to 20 parallel uploads. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps bulk uploads from overwhelming cheap shared hosting while letting capable servers push files as fast as they can.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Killing the Configuration Headache (OAuth vs. JSON Keys)
&lt;/h2&gt;

&lt;p&gt;One more thing was bugging us: the setup experience.&lt;/p&gt;

&lt;p&gt;Most WordPress media offload plugins require you to go into AWS IAM or the Google Cloud console, create a service account, assign bucket policies, download a JSON credentials file, and carefully paste strings into WordPress. It takes 15 minutes, and if you miss a bracket in the IAM policy, nothing works.&lt;/p&gt;

&lt;p&gt;We wanted connecting to Cloudflare R2 or Google Cloud Storage to take 10 seconds, not 10 minutes.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Cloudflare R2&lt;/strong&gt;, we built a &lt;strong&gt;Quick Setup&lt;/strong&gt; flow (PRO only — someone had to write and debug all that token plumbing). You click a button, the plugin opens Cloudflare's token creation page with the right permissions pre-filled, you create the token and paste it back. Buckets auto-populate in a dropdown. Cloudflare doesn't offer OAuth like Google does, but this is as close to one-click as their platform allows.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Google Cloud&lt;/strong&gt;, we registered an OAuth app and integrated it into the PRO version — that part alone took more work than you'd expect. But now you just click "Sign in with Google", authorize, and your buckets appear. No creating service accounts, no navigating the IAM console (which feels like it requires a PhD), no downloading JSON key files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwdzhlc581lf1mz3okai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwdzhlc581lf1mz3okai.png" alt="Google Cloud Setup" width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No copy-pasting IAM policies. No credential files floating around. We still support manual Access Keys for users who need that level of control, but most people pick the quick flow and move on. I don't like clicking through 15 setup screens either -- that was the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: The WooCommerce Edge Case (Signed URLs)
&lt;/h2&gt;

&lt;p&gt;WooCommerce support is a whole separate beast and deserves its own post, but one bug we hit is worth sharing here because it's a good architectural lesson.&lt;/p&gt;

&lt;p&gt;When you need to serve &lt;em&gt;private&lt;/em&gt; digital products, things get tricky. We ran into this integrating with WooCommerce digital downloads. Customers were buying products, but clicking the download link returned &lt;strong&gt;403 Forbidden&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Root Cause:&lt;/strong&gt;&lt;br&gt;
If you serve public assets, you typically map a custom domain to your bucket (e.g., &lt;code&gt;cdn.yourstore.com&lt;/code&gt; -&amp;gt; &lt;code&gt;public-bucket&lt;/code&gt;). But when you generate a temporary Signed URL to securely deliver a protected zip file, that file lives in a &lt;em&gt;private&lt;/em&gt; bucket. &lt;/p&gt;

&lt;p&gt;Our URL generation logic was accidentally bucket-agnostic. When it generated a Signed URL for a file in the private bucket, it was applying the custom domain from the public bucket. The request would hit the public proxy, looking for a private file, and fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: Bucket-Aware Domains&lt;/strong&gt;&lt;br&gt;
We had to rewire how our &lt;code&gt;S3Credentials&lt;/code&gt; class handled routing. Instead of globally applying a custom domain, we implemented strict &lt;strong&gt;bucket-aware custom domains&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now, when WooCommerce asks the plugin for a secure download link, the system checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this file in the primary public bucket? If yes, apply the custom domain.&lt;/li&gt;
&lt;li&gt;Is this file in a private bucket? If yes, bypass the custom domain completely and fall back to the provider's direct API endpoint (like the Cloudflare R2 API) to generate a valid, cryptographically signed URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A small routing change, but it's the difference between a working checkout and customers filing support tickets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We started this project to run WordPress on Google Cloud Run. We ended up rethinking how offload plugins should be built.&lt;/p&gt;

&lt;p&gt;If you're pushing WordPress into serverless or containerized hosting, plugin size matters more than most people think. The vendor SDKs are convenient, but they come with real costs: cold-start latency, deployment bloat, and thousands of extra files in your attack surface.&lt;/p&gt;

&lt;p&gt;What worked for us:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Skip the SDKs. Direct REST API calls with Guzzle handle everything an offload plugin needs to do, at a fraction of the size (67MB down to 2.5MB).&lt;/li&gt;
&lt;li&gt;Queue everything. Background cron keeps uploads from blocking the WordPress admin thread.&lt;/li&gt;
&lt;li&gt;Make setup fast. If connecting a cloud provider takes 15 minutes, something is wrong.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you're dealing with plugin bloat or looking for a lightweight WP Offload Media alternative, &lt;a href="https://wordpress.org/plugins/wp-cloudsync-master/" rel="noopener noreferrer"&gt;CloudSync Master&lt;/a&gt; is free on WordPress.org. Full setup guides and WP-CLI reference are in the &lt;a href="https://github.com/1TeamSoftware/docs/tree/main/plugins/wp-cloudsync-master" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the architecture, serverless WordPress, or cloud provider migration in the comments.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>serverless</category>
      <category>php</category>
    </item>
    <item>
      <title>AI E-Mail Event Booking Agent</title>
      <dc:creator>Dmitry Rechkin</dc:creator>
      <pubDate>Sun, 01 Sep 2024 04:57:23 +0000</pubDate>
      <link>https://dev.to/dmitryrechkin/ai-e-mail-event-booking-agent-16oh</link>
      <guid>https://dev.to/dmitryrechkin/ai-e-mail-event-booking-agent-16oh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/nylas"&gt;Nylas Challenge&lt;/a&gt;: AI Expedition.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built and Why
&lt;/h2&gt;

&lt;p&gt;As a member of the strata council, I regularly handle reservations for the amenity room. While the current volume of bookings isn’t overwhelming, it does come with its own set of challenges—negotiating time slots, enforcing reservation rules, ensuring the completion of PDF forms, verifying them, and then manually booking the room.&lt;/p&gt;

&lt;p&gt;With more residents moving into our building, I began to think about automating this process. However, communicating new methods, such as using specific URLs for reservations, might not work for everyone. That's when the idea of building an AI agent to manage reservations started taking shape in my mind. The Nylas challenge was the final push I needed to put aside other projects and dive into this one.&lt;/p&gt;

&lt;p&gt;Nylas offers the exact tools I was looking for, so I decided to create something practical that we can actually use in our building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Driven by the motto “go PRO or go home,” I might have gone a bit overboard with the video. The real action starts at the 2:30 mark and lasts about 2 minutes.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;a href="https://www.youtube.com/watch?si=shfVYjWjPWrdIO_k&amp;amp;%3Bt=170&amp;amp;v=L_Rw30v6bg0&amp;amp;feature=youtu.be" rel="noopener noreferrer"&gt;
      youtube.com
    &lt;/a&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The source code for the AI email event booking agent can be found on &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dmitryrechkin" rel="noopener noreferrer"&gt;
        dmitryrechkin
      &lt;/a&gt; / &lt;a href="https://github.com/dmitryrechkin/ai-email-event-booking-agent" rel="noopener noreferrer"&gt;
        ai-email-event-booking-agent
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AI Email Event Booking Agent&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;This project implements an AI-powered Email Event Booking Agent designed to automate the process of handling incoming event booking requests via email. The agent intelligently processes emails, validates the information provided, requests additional details if necessary, checks calendar availability, fills out PDF forms, and books events.&lt;/p&gt;
&lt;p&gt;This solution is optimized to run on serverless Cloudflare Workers, aiming to minimize external dependencies for enhanced speed and efficiency. Many packages have been custom-written to support this goal, ensuring that the solution remains lightweight and fast.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Key Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serverless Architecture&lt;/strong&gt;: Runs on Cloudflare Workers, leveraging Cloudflare KV for storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nylas API Integration&lt;/strong&gt;: Handles incoming emails via the Nylas API, transforming them into a common format for AI processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic AI Agent&lt;/strong&gt;: Utilizes AI to decide which tools to use based on natural language instructions and a customizable JSON schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Dependencies&lt;/strong&gt;: Custom code replaces…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dmitryrechkin/ai-email-event-booking-agent" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Your Journey
&lt;/h2&gt;

&lt;p&gt;I'm a huge fan of serverless technology, particularly Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;Initially, I planned to take advantage of the &lt;a href="https://www.npmjs.com/package/nylas" rel="noopener noreferrer"&gt;Nylas SDK&lt;/a&gt;. However, I quickly ran into issues because it wasn’t particularly compatible with Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;No big deal, I figured. Nylas has well-documented APIs, which are explained in detail &lt;a href="https://developer.nylas.com/docs/api/v3/ecc/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The challenge was understanding which combination of APIs would best suit my needs.&lt;/p&gt;

&lt;p&gt;At first, I decided to use the &lt;a href="https://developer.nylas.com/docs/api/v3/ecc/#post-/v3/grants/-grant_id-/events" rel="noopener noreferrer"&gt;Create Events API&lt;/a&gt; and the &lt;a href="https://developer.nylas.com/docs/api/v3/ecc/#post-/v3/grants/-grant_id-/messages/send" rel="noopener noreferrer"&gt;Send Message API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then, I discovered the Nylas Scheduler API, which seemed perfect for my AI Event Booking Agent. You can learn more about it &lt;a href="https://developer.nylas.com/docs/v3/scheduler/using-scheduler-api/#create-a-configuration-object" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, I quickly realized that the requirements to create a configuration object, followed by a session, might be too complex for an AI to handle. So, I reverted to my original plan of using the Email, Calendar, and Event APIs separately.&lt;/p&gt;

&lt;p&gt;Although the webhook provides a snippet of the email, I realized that the body of the email is often in HTML and can contain previous messages embedded within it. This could potentially confuse the AI agent, so I embarked on a journey to find a library that could extract just the latest email body as text, making it easier to feed to the LLM.&lt;/p&gt;

&lt;p&gt;After much searching, I found that nothing worked as expected or required Node.js. So, I implemented my own solution, which turned out to be pretty decent and will likely be used in future projects: &lt;a href="https://github.com/dmitryrechkin/text-email-body-parser" rel="noopener noreferrer"&gt;text-email-body-parser&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One of the requirements was to be able to read and fill PDF forms. While this is relatively easy with existing libraries, I needed a set of shortcut classes capable of reading and filling PDF files represented as byte arrays since working with files was out of the question in this case.&lt;/p&gt;

&lt;p&gt;During the process of implementing my abstraction layer on top of the Nylas API, which could be used in a Node.js-less environment, I created two helper packages: &lt;a href="https://github.com/dmitryrechkin/calendar-nylas" rel="noopener noreferrer"&gt;calendar-nylas&lt;/a&gt; and &lt;a href="https://github.com/dmitryrechkin/message-nylas" rel="noopener noreferrer"&gt;message-nylas&lt;/a&gt;. These packages enable the AI Event Booking Agent to perform the necessary tasks for booking events and sending emails.&lt;/p&gt;

&lt;p&gt;As I researched the best way to implement the agent, I chose Vercel’s AI SDK (&lt;a href="https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling" rel="noopener noreferrer"&gt;AI SDK Core&lt;/a&gt;) because it provides a streamlined method for automating tool calls. This allows me to focus on creating tools and refining prompts instead of worrying about the mechanics.&lt;/p&gt;

&lt;p&gt;To make my code easily adaptable for tool use, I created a tiny library, &lt;a href="https://github.com/dmitryrechkin/foundation-core" rel="noopener noreferrer"&gt;foundation-core&lt;/a&gt;, which lets me create actions in a predictable way and use them as tools with automatic input and output validation using Zod.&lt;/p&gt;

&lt;p&gt;Next, I wanted to define a schema for the data to collect, so I could efficiently extract it with AI and then pass it to my tool. While Zod is amazing for this, I couldn't find a pre-existing solution that allowed dynamic schema definition without generating static code.&lt;/p&gt;

&lt;p&gt;This led to the creation of &lt;a href="https://github.com/dmitryrechkin/json-schema-to-zod" rel="noopener noreferrer"&gt;json-schema-to-zod&lt;/a&gt;, which I can use to store data structures as JSON schemas in KV storage and dynamically convert them to Zod schemas for use with AI tools.&lt;/p&gt;

&lt;p&gt;Finally, it was just a matter of putting everything together. I’ve described the architecture in the repository, so I won’t go into it again here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project was an exciting journey into the world of AI-driven automation. The combination of Nylas, Cloudflare Workers, and Vercel’s AI SDK allowed me to build a practical solution that I can use in my own building. It’s fascinating to see how AI can manage something as complex as event bookings, complete with all the quirks and challenges of real-world scenarios.&lt;/p&gt;

&lt;p&gt;While there were a few hurdles along the way, like adapting the Nylas API for Cloudflare Workers and dealing with the intricacies of parsing email bodies, these challenges ultimately led to the creation of several reusable libraries that I’m sure will be beneficial in future projects.&lt;/p&gt;

&lt;p&gt;In the end, the AI Event Booking Agent not only simplifies the reservation process but also opens the door to more sophisticated AI-driven workflows. I’m excited to see how this project evolves and how AI can further enhance everyday tasks in our work and life.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>nylaschallenge</category>
      <category>api</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
