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.