WordPress Conditional Tags Guide: is_singular, is_main_query, in_the_loop and Common Mistakes

WordPress’s conditional tag functions — the family of functions starting with is_, in_, and has_ — are how themes and plugins determine the context of the current request without parsing the URL manually. They are built on top of the global WP_Query object and are only reliable after WordPress has finished parsing the request — specifically after the parse_query action. Using them too early (in functions.php at file level, or on init before the query is set up) returns incorrect results. This article covers the most important conditional tags for WordPress developers: the ones that are commonly misused, the ones that behave differently in plugin context versus theme context, and the critical difference between is_singular() vs is_single(), is_main_query() vs checking the post type, and in_the_loop() vs is_single().

Problem: Your plugin's pre_get_posts callback modifies a query that is supposed to apply only to the main front-end archive, but it also triggers on admin queries, on custom WP_Query instances in widgets, and on REST API requests — breaking unrelated queries across the site.

Solution: Use is_admin(), $query->is_main_query(), and specific conditional tags together to target exactly the right context. Understand what each check covers and combine them to narrow scope.

<?php
// ── pre_get_posts: the canonical pattern for safe query modification ───
add_action( 'pre_get_posts', function ( WP_Query $query ) {
    // is_admin()         → true for all wp-admin requests (including AJAX)
    // is_main_query()    → true only for the primary query on the page
    if ( is_admin() || ! $query->is_main_query() ) {
        return; // leave admin queries and secondary loops untouched
    }
    if ( $query->is_post_type_archive( 'event' ) ) {
        $query->set( 'orderby', 'meta_value' );
        $query->set( 'meta_key', 'event_date' );
        $query->set( 'order', 'ASC' );
    }
} );

// ── is_single() vs is_singular() ──────────────────────────────────────
is_single();        // true only for single posts (post_type = 'post')
is_singular();      // true for single post, page, or any custom post type
is_singular('movie'); // true only for single 'movie' CPT posts
is_page();          // true only for single pages (post_type = 'page')

// ── is_archive() family ────────────────────────────────────────────────
is_archive();               // true for any archive: category, tag, date, author, CPT archive
is_post_type_archive();     // true for any CPT archive
is_post_type_archive('event'); // true only for 'event' CPT archive
is_category();              // true for category archives
is_tag();                   // true for tag archives
is_tax( 'genre' );          // true for 'genre' taxonomy archives
is_author();                // true for author archive pages
is_date();                  // true for date-based archives

// ── in_the_loop() — are we inside a WP_Query loop? ────────────────────
// Useful in filters like the_content to avoid running on non-loop contexts
add_filter( 'the_content', function ( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content; // only modify main post body, not excerpts or widgets
    }
    return $content . '<p class="share-prompt">Share this article!</p>';
} );

// ── Timing: conditional tags only work AFTER wp action ────────────────
// WRONG: using is_single() on 'init' — WP_Query not yet set up
add_action( 'init', function () {
    if ( is_single() ) { /* always false here */ }
} );

// CORRECT: use 'wp' or 'template_redirect' hook (after query is parsed)
add_action( 'template_redirect', function () {
    if ( is_single() ) { /* reliable here */ }
} );

NOTE: is_admin() returns true for ALL admin-area requests, including AJAX requests made from the admin. If you need to check whether the request is a front-end AJAX call (initiated from a public page), combine with wp_doing_ajax(): if ( is_admin() && ! wp_doing_ajax() ). Also, conditional tags check the global query state — inside a custom new WP_Query() loop, global conditionals like is_single() still reflect the main query, not your custom query. Use the method versions on the query object directly: $my_query->is_singular().