Migrate Legacy Content into Drupal with YAML and Drush
Use Drupal's Migration API to import CSV or legacy content into nodes declaratively, then re-run or rollback safely with Drush.
21 Apr 2026, 22:03 UTC

The problem: legacy content is stuck, and launch is near
You have a Drupal 10 or 11 site ready for launch, but the content still lives in an old system—CSV exports, a previous Drupal version, or another CMS. Copy-pasting articles one by one is slow and error-prone, and writing a custom PHP importer means maintaining brittle code every time the source file changes.
Drupal's Migration API offers a declarative alternative. You define the source, the field transformations, and the destination in YAML. Drush, Drupal's command-line interface, executes the import, tracks every source-to-destination pair, and lets you roll back the changes if something goes wrong.
How source, process, and destination fit together
A migration is an ETL pipeline: extract from a source, transform through process plugins, and load into a destination plugin. Common sources include CSV, JSON, SQL, and Drupal 7 nodes. Process plugins handle field mapping, lookups, and defaults. The destination writes to a Drupal entity, such as a node or user.
Each migration run records its work in a map table. On re-runs, Drupal skips source rows that already have a destination, which makes imports idempotent. A sortable unique source ID—called a highwater mark—lets you resume interrupted imports or import only new rows.
Worked example: importing CSV articles into nodes
Assumptions: Drupal 10.x or 11.x, Drush installed, and the Migrate and Migrate Tools modules enabled. Place the YAML where your migration configuration is discovered, such as a custom module's migration directory or your configuration sync path. Migration YAML files are configuration entities. In a deployment workflow, include them in your configuration sync or manage them with config_split; do not treat them as ordinary module code.
Create article_import.yml:
langcode: en
status: true
id: article_import
label: 'Import articles from CSV'
migration_group: legacy_content
source:
plugin: csv
path: 'modules/custom/my_migrate/data/articles.csv'
header_row_count: 1
ids:
- legacy_id
destination:
plugin: 'entity:node'
process:
type:
plugin: default_value
default_value: article
title: title
body/value: body
field_author:
plugin: migration_lookup
migration: user_import
source: author_email
migration_dependencies:
required:
- user_import
Run the import from your site root:
drush migrate:import article_import --limit=5
Run this as a user with database access and read permission on the CSV file. Running as the wrong system user can leave imported files unreadable by the web server or cause the import to fail partway through.
Check the result with:
drush migrate:status
You should see article_import with Imported: 5 and Status: Idle. Run the same command again; it should import zero additional rows because the migration map already records those source IDs.
Inspect the tracking table directly:
SELECT * FROM key_value WHERE collection = 'migrate_map_article_import';
Each row maps a source legacy_id to a Drupal node ID and stores a hash used to detect changes.
Roll back when the import is wrong
Because importing creates nodes, rollback is available:
drush migrate:rollback article_import
This deletes the nodes created by this migration. Only run it when you are certain the destination data is disposable, such as in a development or staging environment.
Trade-offs to accept before you start
- The Migration API is built for bulk or scheduled imports, not real-time synchronization. For continuous updates, pair it with a message queue and event subscribers instead of polling.
- Highwater marks require a sortable unique source ID. Sources without one need a custom ID map or hash-based tracking.
- Rollback only works if the destination plugin supports it. Core entity destinations do; custom destinations may not.
- Large CSV or JSON files can exhaust memory. Use chunked SQL sources or streaming parsers for very large datasets.
- Plugin availability changes between versions. Confirm your CSV source plugin before upgrading to Drupal 11, where older CSV source plugins have been removed.
Verify before you cut over
After the import, open /admin/content and confirm the expected number of article nodes exists. Compare a sample of source rows against their destination nodes to verify field mapping. If the counts and spot checks match, the migration is ready for a larger run.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.