Manage WordPress Multisite network sites programmatically with PHP

WordPress Multisite (enabled with define(‘WP_ALLOW_MULTISITE’, true) in wp-config.php) lets you run a network of WordPress sites from a single codebase and database installation. Each site in the network gets its own set of tables (prefixed with the site ID, e.g. wp_2_posts), its own settings, theme, and plugins, but they all share the same WordPress core files and a single user table. This makes multisite ideal for agency setups (one install per client), university networks (one site per department), and SaaS platforms with tenant-specific subdomains. From a developer’s perspective, the key multisite-specific functions are: get_sites() (query all sites in the network), switch_to_blog() / restore_current_blog() (switch database context to a different site), add_site() (create a new site programmatically), and is_multisite() / is_main_site() (context detection). The switch_to_blog() function is the most important — it switches all WordPress globals and $wpdb table names to the target site, so any WordPress function you call after it operates on that site’s data. Always call restore_current_blog() after you’re done or you will corrupt the rest of the request. Setting up WordPress Multisite from scratch was covered in the Multisite setup guide; this article focuses on programmatic site management for plugin and theme developers.

Problem: You need to programmatically create new sites in a WordPress Multisite network, query all sites, and run operations across multiple sites from a single request.

Solution: Add the following code to your network plugin or mu-plugin:

// Check multisite context
if ( ! is_multisite() ) {
    return; // this code only runs on multisite installs
}

// Query all sites in the network
$sites = get_sites( [
    'number'   => 100,
    'orderby'  => 'registered',
    'order'    => 'ASC',
    'archived' => 0,
    'deleted'  => 0,
] );

foreach ( $sites as $site ) {
    echo $site->blog_id . ': ' . $site->domain . $site->path . PHP_EOL;
}

// Switch to a specific site and run a query
function helloadmin_count_posts_on_site( int $blog_id ): int {
    switch_to_blog( $blog_id );
    $count = (int) wp_count_posts( 'post' )->publish;
    restore_current_blog(); // always restore!
    return $count;
}

// Run an operation on every site in the network
function helloadmin_network_update_option( string $key, mixed $value ): void {
    $sites = get_sites( [ 'fields' => 'ids' ] );
    foreach ( $sites as $blog_id ) {
        switch_to_blog( $blog_id );
        update_option( $key, $value );
        restore_current_blog();
    }
}

// Create a new site programmatically
function helloadmin_create_site( string $subdomain, string $title, int $user_id ): int|WP_Error {
    return wp_insert_site( [
        'domain'  => $subdomain . '.' . DOMAIN_CURRENT_SITE,
        'path'    => '/',
        'title'   => sanitize_text_field( $title ),
        'user_id' => $user_id,
    ] );
}

// Add a user to all sites in the network
function helloadmin_add_user_to_all_sites( int $user_id, string $role = 'subscriber' ): void {
    $sites = get_sites( [ 'fields' => 'ids' ] );
    foreach ( $sites as $blog_id ) {
        add_user_to_blog( $blog_id, $user_id, $role );
    }
}

// Network-wide option (stored in wp_sitemeta, not wp_options)
update_site_option( 'helloadmin_network_setting', 'value' );
$setting = get_site_option( 'helloadmin_network_setting', 'default' );

NOTE: switch_to_blog() changes the global $wpdb table prefix and several WordPress globals. If you call it inside a hook and forget restore_current_blog(), every subsequent WordPress operation in that request will run against the wrong site’s database — a very hard-to-debug problem. Always use a try/finally block if there is any chance an exception could skip your restore_current_blog() call. On large networks (100+ sites), looping over every site with switch_to_blog() is slow — consider using direct $wpdb queries against the specific site tables instead.