How to Use Drupal’s Migrate API to Import Data from CSV
Moving structured content into Drupal is a common task when a site is rebuilt, a spreadsheet becomes a content system, or an organisation replaces an older CMS. CSV files are especially useful because they can be exported from Excel, Google Sheets, databases, and many business applications.
Drupal’s Migrate API provides a controlled way to read rows from a source, transform values, and create Drupal entities. A migration can import nodes, users, taxonomy terms, media, commerce products, or custom content entities while preserving relationships between records.
A reliable migration is more than a quick upload. It should handle empty fields, duplicate records, dates, references, formatting differences, and errors that need to be corrected before production. This is particularly important when importing Australian addresses, where postcodes may contain leading zeroes and dates are often written as day/month/year.
The examples below use Drupal 10 or Drupal 11 concepts, Drush, and migration configuration in YAML. The same approach can be adapted for a local development environment, a staging server, or a large production website.
Prepare Drupal And The CSV File
Start by identifying the destination content model. For example, a CSV containing title, summary, suburb, postcode, publication date, and category could be imported into an Article content type. Create the required fields first, including the correct field types. A postcode should usually be stored as text rather than an integer, because values such as 0800 must retain their leading zero.
The CSV should use UTF-8 encoding and a consistent delimiter. A simple file might look like this:
source_id,title,summary,suburb,postcode,published,category
101,Local Market Update,"Weekly news from the market",Newtown,2042,15/02/2025,Community
102,Coastal Walk Guide,"A guide to the local track",Fremantle,6160,20/02/2025,Travel
Check whether the first row contains headers, whether values are enclosed in quotes, and whether line breaks appear inside cells. Spreadsheet programs can introduce unexpected characters, especially when a file has been saved on Windows and processed on Linux. Keep a stable source identifier in the file; it will be used to track imported records and support future updates.
For practical Drupal administration and development references, the Drupalwoo blog provides a useful context for configuring modules and maintaining Drupal sites.
Install The Migration Tools
Drupal’s Migrate API is part of Drupal core, but importing CSV data generally requires a CSV source plugin. A common setup uses Migrate Plus for additional source and process plugins and Migrate Tools for Drush commands. Install them with Composer from the project root:
composer require drupal/migrate_plus drupal/migrate_tools
drush en migrate_plus migrate_tools -y
The exact module requirements can vary by Drupal version and project. Migrate Plus supplies the csv source plugin in commonly used configurations, while Migrate Tools adds commands such as migrate:import, migrate:status, and migrate:rollback. Confirm the available plugin documentation for the installed release rather than copying configuration from an unrelated major version.
Place custom migrations in a custom module, for example:
web/modules/custom/acme_migration/
acme_migration.info.yml
acme_migration.migration.yml
acme_migration.module
data/articles.csv
The module’s .info.yml file should declare dependencies on migrate, migrate_plus, and migrate_tools where appropriate. Keep the CSV outside the public web root when it contains private information, and restrict file permissions so that a web visitor cannot download it.
Define The CSV Source
A migration definition connects a source plugin to a process section and a destination plugin. The source section tells Drupal where the CSV file is located, whether it has headers, and which column identifies each row.
A basic configuration can be written as follows:
id: acme_articles
label: 'Import articles from CSV'
migration_group: acme
source:
plugin: csv
path: modules/custom/acme_migration/data/articles.csv
header_row_count: 1
ids:
source_id:
type: integer
fields:
- source_id
- title
- summary
- suburb
- postcode
- published
- category
process:
title: title
body/value: summary
body/format:
plugin: default_value
default_value: basic_html
field_suburb: suburb
field_postcode: postcode
destination:
plugin: 'entity:node'
default_bundle: article
The id must be unique within the Drupal installation. The ids setting is crucial because Drupal uses it to record which source row created each destination entity. If source_id is not unique or changes between imports, updates and rollbacks may behave unexpectedly.
For a file outside the module directory, use an absolute path or a path token supported by the source plugin and deployment process. A production-friendly approach is to keep the file in a controlled private directory and provide the path through configuration management or an environment-specific setting.
Map And Transform Field Values
The process section maps source columns to Drupal fields. Direct mappings work when the source data already matches the destination field. Real imports usually need transformations. A date written as 15/02/2025, for example, may need to become a Unix timestamp or an ISO-compatible value before it can be saved to a Drupal date field.
A process pipeline can combine plugins:
process:
title:
plugin: callback
callable: trim
source: title
field_postcode:
plugin: callback
callable: trim
source: postcode
field_published:
plugin: format_date
source: published
from_format: 'd/m/Y'
to_format: 'U'
field_category:
plugin: entity_lookup
source: category
value_key: name
bundle_key: vid
bundle: article_category
entity_type: taxonomy_term
ignore_case: true
The entity_lookup example assumes that matching taxonomy terms already exist. If a category is missing, the migration may leave the field empty or report an error depending on the configuration. A separate taxonomy migration is often the cleaner solution: import terms first, then import articles that reference them.
Common process plugins include default_value, skip_on_empty, static_map, explode, concat, substr, migration_lookup, and entity_lookup. Use migration_lookup when the CSV contains an identifier from another migration, such as a category source ID or an author ID. This creates a dependable relationship between imported entities instead of matching on display text.
Handle References And Multiple Values
CSV files often represent multi-value data in one cell, such as News|Events|Local. Drupal expects separate values for a multi-value taxonomy or entity reference field. The explode process plugin can split the cell, but the resulting values still need to match valid target entity IDs or be passed through an entity lookup process.
For related records, create separate migrations. A venue migration might import locations first, using venue_id as its source identifier. An event migration can then use migration_lookup to convert its venue_id into the Drupal entity ID required by an entity reference field. This makes the import order explicit and helps Drupal understand dependencies.
process:
field_venue:
plugin: migration_lookup
migration: acme_venues
source: venue_id
Add migration dependencies when one migration must run before another:
migration_dependencies:
required:
- acme_venues
If the source contains HTML, decide how much markup is safe before importing it into a formatted text field. Do not assume that spreadsheet content is clean. Strip unwanted tags, use an appropriate text format, and review content containing ampersands, apostrophes, or smart quotes. Australian organisations often import suburb and council data from separate systems, so normalising names before matching can prevent duplicate taxonomy terms such as “St Kilda” and “St. Kilda”.
Run Test Imports With Drush
After enabling the custom module, inspect the migration:
drush cr
drush migrate:status
Import a small batch first:
drush migrate:import acme_articles --limit=10
The --limit option is helpful during development, while --feedback=1 can display progress for larger files. Review the created nodes, field values, taxonomy references, aliases, and publication status in the Drupal interface. Also inspect watchdog or recent log messages when a row fails.
If a migration is interrupted, running the import again generally processes unfinished rows. When a configuration change affects already-imported records, roll back the migration in a development environment before re-running it:
drush migrate:rollback acme_articles
drush migrate:import acme_articles
Rollback removes entities created by that migration, so take care when testing against shared staging data. A backup is sensible before a large production import. For a Sydney, Melbourne, or Brisbane site with thousands of records, run the command from a terminal session that will not disconnect, and monitor server memory and execution limits.
Use migrate:messages to inspect migration-specific errors:
drush migrate:messages acme_articles
Rows with invalid dates, missing references, or unexpected encodings should be corrected at the source where possible. Temporary workarounds in YAML can help, but repeatedly compensating for poor source data makes future imports harder to maintain.
Make The Migration Repeatable
A migration becomes valuable when it can be run more than once. The source identifier allows Drupal to distinguish an existing record from a new one, but updates work best when the source data remains stable and the migration definition is predictable. Do not use a row number that changes whenever the CSV is sorted; use a permanent ID from the source system.
Consider whether the migration should update titles, body text, editorial fields, or only create content once. Some fields may be maintained by Drupal editors after the initial import. In that case, importing every source value on every run could overwrite legitimate editorial changes. Document which fields are authoritative in the source system.
The following reference makes the main configuration choices easier to compare:
| Migration Concern | Typical Configuration | Important Check |
|---|---|---|
| CSV reader | plugin: csv |
Confirm the required contrib module is enabled |
| Row identity | ids: source_id |
Use a stable, unique source identifier |
| Text field | title: title |
Trim whitespace and review encoding |
| Date conversion | format_date |
Match the source format, such as d/m/Y |
| Taxonomy matching | entity_lookup |
Ensure terms exist before importing |
| Related migration | migration_lookup |
Add the required migration dependency |
| Trial run | --limit=10 |
Inspect content before a full import |
| Error review | migrate:messages |
Fix invalid rows and repeat the test |
Validate The Imported Content
Technical success does not guarantee a useful result. After the import, check a representative sample from every content type and source category. Verify that titles are complete, summaries have the expected formatting, dates display correctly, and references point to the intended terms or entities.
Pay close attention to Australian data conventions. A date such as 03/04/2025 is ambiguous if the source system expects month/day/year, while Drupal sites serving local audiences normally need day/month/year interpretation. Postcodes such as 0800 in Darwin and 0870 in Alice Springs must not be converted into numbers. Names containing macrons, apostrophes, or Aboriginal language characters should survive the CSV-to-Drupal process without being replaced by question marks.
Check URL aliases, menu links, author ownership, moderation states, and access permissions as well. If the imported records are intended for public release, keep them unpublished until content owners have reviewed the data. For a council, university, retailer, or community organisation, sample records from several states and business units rather than checking only the first few rows.
Once the migration is trusted, store the YAML and source-processing instructions in version control. Keep a copy of the original CSV, record its export date, and document the commands used. That audit trail makes a later migration from a CRM, membership platform, or legacy website far less risky.