Export Every WordPress Taxonomy with WP-CLI
If you’ve ever tried to get a complete picture of every taxonomy and term registered on a large WordPress site, you’ve felt the pain. The database stores terms and their relationships, but not the human-readable taxonomy labels or registered post type associations. Those live in PHP. So, while a raw SQL query might get you halfway there, a quick WP-CLI script takes you all the way.
Recently we needed full inventory of a client site with over 25 registered taxonomies across 16 post types and almost 400 individual terms, and realized there was no built-in way to get one.
Why SQL Won’t Cut It for Taxonomy Exports
WordPress splits taxonomy data across the database and the codebase. The wp_terms and wp_term_taxonomy tables give you term names, slugs, IDs, parent relationships, and the taxonomy machine name (e.g., post_tag, category, product_cat). That’s useful, but it’s not the full picture.
Two critical pieces are only available at runtime through PHP. First, the singular label: the human-readable name like “Tag” or “Product Category” that’s defined when the taxonomy is registered with register_taxonomy(). Second, the script captures the registered post type associations and shows which post types the taxonomy officially attaches to. You can infer post types from wp_term_relationships and wp_posts, but that only tells you what’s currently in use, not what’s registered. An empty taxonomy with no assigned posts won’t show up at all.
The script below pulls everything from a single source: the WordPress runtime, where both database records and PHP registrations are available.
The Script
Drop this code into a file export-taxonomies.php into your WordPress root directory.
<?php
/**
* Export All WordPress Taxonomy Terms to CSV
*
* Usage: wp eval-file export-taxonomies.php
* Output: taxonomy-export_{timestamp}.csv in the WordPress root directory
*
* Columns:
* Term Name, Term Slug, Term ID, Parent Name, Parent ID,
* Taxonomy Label (Singular), Taxonomy Name (Machine), Post Types (Registered)
*/
if ( ! defined( 'ABSPATH' ) ) {
WP_CLI::error( 'This script must be run with: wp eval-file export-taxonomies.php' );
}
$timestamp = date( 'Y-m-d_H-i-s' );
$output_file = ABSPATH . "taxonomy-export_{$timestamp}.csv";
$fp = fopen( $output_file, 'w' );
if ( ! $fp ) {
WP_CLI::error( "Could not open {$output_file} for writing." );
}
// CSV header row.
fputcsv( $fp, [
'Term Name',
'Term Slug',
'Term ID',
'Parent Name',
'Parent ID',
'Taxonomy Label (Singular)',
'Taxonomy Name (Machine)',
'Post Types (Registered)',
] );
// Get all registered taxonomies (public and private).
$taxonomies = get_taxonomies( [], 'objects' );
$row_count = 0;
foreach ( $taxonomies as $tax_slug => $tax_obj ) {
// Skip nav_menu — it's internal noise.
if ( $tax_slug === 'nav_menu' ) {
continue;
}
$singular_label = $tax_obj->labels->singular_name ?? $tax_obj->label;
// Map post type slugs to their singular labels.
$post_type_labels = array_map( function( $pt_slug ) {
$pt_obj = get_post_type_object( $pt_slug );
if ( $pt_obj ) {
return $pt_obj->labels->singular_name . ' (' . $pt_slug . ')';
}
return $pt_slug; // Fallback if not registered.
}, $tax_obj->object_type );
$post_types = implode( ', ', $post_type_labels );
// Get every term in this taxonomy (including empty ones).
$terms = get_terms( [
'taxonomy' => $tax_slug,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
] );
if ( is_wp_error( $terms ) || empty( $terms ) ) {
// Still output a row so you know the taxonomy exists, just with no terms.
fputcsv( $fp, [
'(no terms)',
'',
'',
'',
'',
$singular_label,
$tax_slug,
$post_types,
] );
$row_count++;
continue;
}
foreach ( $terms as $term ) {
$parent_name = '';
$parent_id = '';
if ( $term->parent > 0 ) {
$parent_term = get_term( $term->parent, $tax_slug );
if ( $parent_term && ! is_wp_error( $parent_term ) ) {
$parent_name = $parent_term->name;
$parent_id = $parent_term->term_id;
}
}
fputcsv( $fp, [
$term->name,
$term->slug,
$term->term_id,
$parent_name,
$parent_id,
$singular_label,
$tax_slug,
$post_types,
] );
$row_count++;
}
}
fclose( $fp );
WP_CLI::success( "Exported {$row_count} rows to {$output_file}" );
Then run it with WP-CLI:
% wp eval-file export-taxonomies.php
It outputs a timestamped CSV (e.g., taxonomy-export_2026-02-25_14-30-45.csv) with one row per term, covering every registered taxonomy on the site. As part of good website hygiene, delete the script and CSV files from your WordPress root once you’ve captured what you need.
What You Get
Each row in the CSV represents a single term. Here’s what the columns look like in practice:
| Column | Example (Tag) | Example (Custom Taxonomy) |
|---|---|---|
| Term Name | AI | Hoodies |
| Term Slug | ai | hoodies |
| Term ID | 135 | 423 |
| Parent Name | Clothing | |
| Parent ID | 385 | |
| Taxonomy Label (Singular) | Tag | Product Category |
| Taxonomy Name (Machine) | post_tag | product_cat |
| Post Types (Registered) | Post (post), Event (tribe_events) | Product (product) |
The Post Types column includes both the singular label and the machine name in parentheses. This is important when you’re working with sites that have custom post types with cryptic slugs. tribe_events doesn’t mean much at a glance, but “Event (tribe_events)” is immediately clear.
How It Works
The script calls get_taxonomies() with the objects parameter to retrieve every registered taxonomy as a full WP_Taxonomy object. This gives access to labels->singular_name and object_type, the two pieces you can’t get from the database alone.
For each taxonomy, it fetches all terms with get_terms() using hide_empty => false so that terms with no assigned posts still appear. Parent relationships are resolved with a second get_term() call to retrieve the parent’s name. Post type slugs are mapped to their singular labels via get_post_type_object(), with a fallback to the raw slug if the post type isn’t registered (which can happen with leftover data from deactivated plugins).
You’ll even find a row for every empty taxonomy in the CSV, marked “(no terms),” giving you a complete picture of what’s registered even when nothing has been tagged yet. The only taxonomy excluded is nav_menu, which is WordPress’s internal structure for navigation menus, and adds noise without value.
When You Need This
This script is useful any time you need a complete taxonomy inventory. Common scenarios include auditing a site before a migration or redesign, documenting custom taxonomies for a client handoff, identifying orphaned terms from deactivated plugins, verifying that custom post type and taxonomy registrations are correct, and building a content model reference for a development team.
The timestamped filename means you can run it before and after a restructuring to compare the state of your taxonomies at different points.
What is WP-CLI?
WP-CLI is the official command-line interface for WordPress. It lets you manage WordPress installations without a browser: running updates, managing users, importing content, and executing PHP scripts in the WordPress context. The wp eval-file command used here loads WordPress, then runs a PHP file with full access to the WordPress API. If you have SSH access to your server, you almost certainly have WP-CLI available. Talk to your web host to find out for sure.
Get a Clear View of Your Taxonomy Structure
Having a clear view of everything registered on a site: the labels, the attachments, not just raw database entries, cuts your planning time every time you scope out changes. Run this script, open the CSV, and you’ll have a complete reference in seconds.
(Last Updated: February 25, 2026)
This post was developed in collaboration with Claude 4.6 Opus, ChatGPT 5.2 Thinking & Gemini 3 Pro. Image generated using Midjourney 7 and Adobe Firefly via Photoshop. The final content was edited, tested, formatted, and fact-checked by Erik Lutenegger.
Read Other Technical Snippets
[email protected]
Need help with a massive set of taxonomies and content?