<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Three Spire Blog]]></title><description><![CDATA[Three Spire Blog]]></description><link>https://blog.threespires.io</link><image><url>https://substackcdn.com/image/fetch/$s_!JKh_!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34e1823c-e23f-41e0-a12e-43a90cb36a70_403x403.png</url><title>Three Spire Blog</title><link>https://blog.threespires.io</link></image><generator>Substack</generator><lastBuildDate>Wed, 15 Apr 2026 17:39:06 GMT</lastBuildDate><atom:link href="https://blog.threespires.io/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Three Spires Blog]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[threespires@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[threespires@substack.com]]></itunes:email><itunes:name><![CDATA[Three Spires Blog]]></itunes:name></itunes:owner><itunes:author><![CDATA[Three Spires Blog]]></itunes:author><googleplay:owner><![CDATA[threespires@substack.com]]></googleplay:owner><googleplay:email><![CDATA[threespires@substack.com]]></googleplay:email><googleplay:author><![CDATA[Three Spires Blog]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[AI-assisted Drupal ↔ Drupal migrations]]></title><description><![CDATA[Or: how I saved myself a "migraine" with LLMs and helper scripts.]]></description><link>https://blog.threespires.io/p/ai-assisted-drupal-drupal-migrations</link><guid isPermaLink="false">https://blog.threespires.io/p/ai-assisted-drupal-drupal-migrations</guid><dc:creator><![CDATA[Three Spires Blog]]></dc:creator><pubDate>Thu, 27 Mar 2025 09:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!13ww!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few weeks ago, during my time at <a href="https://www.readingroom.com/">Reading Room</a>, I took on the task of writing the <a href="https://www.drupal.org/">Drupal</a> 7 to Drupal 10 migrations on an upgrade job for a UK healthcare site.</p><p>If you are familiar with this sort of thing, you&#8217;ll know this crosses the D7/8 boundary, so typically would involve building the new site fresh, and then using core&#8217;s <a href="https://www.drupal.org/docs/drupal-apis/migrate-api/migrate-api-overview">Migration API</a> to lift-and-shift content from old to new.</p><p>This time was no exception.</p><p>In fact, the fantastic team had already pretty-much built the new site and planned out the data mapping in some detail. I just needed to connect the dots with (in the end) about fifty migrations. Roughly one-per-entity-type/bundle. Each defined in a nice tidy YAML configuration file.</p><p><em><strong>Well</strong></em>. </p><p>(Thought I)</p><p>This sounds like a job for AI!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!13ww!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!13ww!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!13ww!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!13ww!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!13ww!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!13ww!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png" width="1024" height="608" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:608,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!13ww!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!13ww!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!13ww!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!13ww!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F879a2b41-7957-4503-ac11-ac917ede35ba_1024x608.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Man and woman super hero with "AI" emblem</figcaption></figure></div><p>After all, I already have most of the mapping logic written in human words (on tickets). I just need to transform it into Drupal migration YAML text files. <em>And one thing LLMs are really good at, is rewriting text from one genre to another.</em></p><p>Right?</p><div><hr></div><p><em>At this point let me give a thank you and general shout out to the fabulous <a href="https://www.readingroom.com/">Reading Room</a>, the team behind this project and one of the driving forces behind the recent, wildly successful <a href="https://www.drupalcampengland.org/">DrupalCamp England 2025</a>. I&#8217;ve been working with them for a while now, and can attest to their significant Drupal expertise and Digital Transformation know-how.</em></p><div><hr></div><h2>My migration-making workflow</h2><p>The basic idea is to create a markdown file that describes the migration. It has the following structure:</p><pre><code># INSTRUCTIONS

Write a Drupal 7 to Drupal 10 migrate_plus migration yml into config/sync/ for the migration specified in this file.

...(add more instructions here, specific to your use case and preferences)

## Entity Mapping

Source: node (article)
Destination: node (article)

## Source Field Details

| FIELD NAME                    | TYPE                | CARDINALITY |
| ----------------------------- | ------------------- | ----------- |
| nid                           | integer             | 1           |
| body                          | text_with_summary   | 1           |

...(etc)

## Destination Field Details

| FIELD NAME                    | TYPE                | CARDINALITY |
| ----------------------------- | ------------------- | ----------- |
| nid                           | integer             | 1           |
| body                          | text_with_summary   | 1           |

...(etc)

## Field mapping

...Unambiguous mapping instructions written in whatever way makes sense to you and yields good results from your LLM of choice. Bullet points? A table? Up to you.</code></pre><p>Obviously, those &#8220;Field Details&#8221; sections can be auto-generated from examining the configuration of the source/destination sites. Which is where <a href="https://github.com/jamsilver/migraine">Migraine</a> comes in. Assuming you&#8217;re <a href="https://github.com/jamsilver/migraine?tab=readme-ov-file#installationrequirements">set up with Migraine</a>, you can ask it to generate that prompt file for a certain migration:</p><pre><code>mise run mig:prompt node_article</code></pre><p>This creates a prompt file in <code>.migraine/prompts/node_article.yml</code>. Open that up and edit it until you&#8217;re happy.</p><p>Then stick some example migrations into <code>.migraine/templates/migrations</code> and pass it all to <a href="https://aider.chat/">Aider</a> to generate the first draft of your migration yaml with this:</p><pre><code>mise run mig:aider:migrate node_article</code></pre><p>This is a wrapper for the following aider command:</p><pre><code>aider \
  --map-tokens=0 \
  --read=".migraine/prompts/node_article.md" \
  --read=".migraine/templates/migrations" \
  --message="Read .migraine/prompts/node_article.md and follow all instructions."</code></pre><p>Configuring <a href="https://aider.chat/">Aider</a> is beyond the scope of this article, but I <a href="https://github.com/jamsilver/migraine?tab=readme-ov-file#example-aider-configuration">give an example of the configuration I used here</a>. I liked architect mode with OpenAI o1 as the chat model, and Anthropic Claude Sonnet as the editor model.</p><p>Perhaps you&#8217;re adding to, or changing, an existing migration? Perhaps you used <a href="https://www.drupal.org/project/migrate_upgrade">Migrate Upgrade</a> to generate your migrations and you exported the config? Pass the file path and aider will update that file:</p><pre><code>mise run mig:aider:migrate node_article \
  config/sync/migrate_plus.migration.node_article.yml</code></pre><p>After all this you&#8217;ll typically want to (re-)import that migration file and re-run the migration to test it. This sort of thing:</p><pre><code>drush cis "../config/sync/migrate_plus.migration.node_article.yml" -y
drush migrate:reset-status node_article
drush migrate:rollback node_article
drush migrate:import node_article</code></pre><p>(The <code>drush cis</code> command is provided by the <a href="https://www.drupal.org/project/config_import_single">Config Import Single</a> module).</p><p>The Migraine scripts are a little &#8220;rough-and-ready&#8221;, but there are lots of things you can do with them (including dumping a full description of all your entity types, which is quite useful to gain a quick understanding of any site). <a href="https://github.com/jamsilver/migraine?tab=readme-ov-file#the-set-up">For more details check out the project page on Github</a>.</p><p>And if you find Migraine helpful, please do raise PRs for improvements.</p><h2>The conclusion: Was AI worth it?</h2><p>Yes. I think so. <em><strong>Just about</strong></em>. For making the first draft of a migration yaml definition file in particular.</p><p>You&#8217;ve got to think about it in the right way. You&#8217;re still gonna go through each migration&#8212;each field mapping&#8212;one by one. You&#8217;re still gonna have to understand the content models, the data structures, migrations, the yaml definitions. You&#8217;re still gonna inspect databases and your destination site to verify imports. <em>There is no free lunch here.</em></p><p>But.</p><p>The question you need to ask is this: Would you rather start from a blank canvas (or <a href="https://www.drupal.org/project/migrate_upgrade">Drupal&#8217;s auto-generated base</a>) only? Or would you rather start from something guessed by an LLM?</p><p>I found that, as long as I sent good examples that exhibited the patterns I wanted to use&#8212;as long as I used a good reasoning model (I used <a href="https://openai.com/o1/">Open AI&#8217;s o1</a>, which was slow but good)&#8212;then getting an LLM to generate the first draft wasn&#8217;t <em>perfect</em>, but it generally got me going faster than otherwise.</p><p>Also, I quickly realised, the process of preparing the AI prompt document was more-or-less <em>identical</em> to the process of marshalling my own understanding of the migration. Cross-referencing JIRA tickets, documentation, asking questions of other developers, other stakeholders, analysing the codebase, the database. I needed to do all that <em>anyway</em>. So why not write it all down in a single Markdown file? </p><p>And, once I&#8217;d done that, why not send that over to an LLM to have a go at making the first draft?</p><p>I did find, towards the end of the project I used AI less and less, because I&#8217;d got to the point where I&#8217;d learnt everything important. So it was quicker to go into this or that migration yaml and copy and paste this or that snippet.</p><p>But, that&#8217;s the point of AI isn&#8217;t it? It helps us make more progress while we&#8217;re still learning, and takes a back seat once we&#8217;ve improved. So, I&#8217;m not surprised by this, and I don&#8217;t think it&#8217;s a bad thing.</p><h2>What&#8217;s next?</h2><p>Well. I&#8217;ve finished that migration project now. And the AI space is moving fast. It&#8217;s difficult to say what comes next.</p><p>But if you found any of this helpful&#8212;or if you know of better ways&#8212;please reach out to me and let&#8217;s discuss our experiences. I&#8217;m available on LinkedIn here: <a href="https://www.linkedin.com/in/jamsilver/">https://www.linkedin.com/in/jamsilver/ </a></p><p>I am convinced that intelligent incorporation of LLMs has the power to make software development <em>better</em>. More fun, even. Gives us all a leg up and gets us to the <em>interesting</em> bits, the <em>important</em> bits faster. With more of our brain cells still intact.</p><p>Thanks for reading.</p>]]></content:encoded></item></channel></rss>