What It Is

The getRowCount() method returns the number of records in a GlideRecord query result set after the query has been executed. It solves the fundamental problem of determining how many records match your criteria when you need both the count and the ability to iterate through those records. This isn't just about counting—it's about understanding the scope of your data before you process it, which is critical for performance planning, pagination logic, and conditional processing based on result size.

Architecturally, getRowCount() executes exclusively on the server side—you'll find it in Business Rules, Script Includes, Workflow scripts, and server-side UI Actions, but never in Client Scripts or UI Policies. It sits at the data access layer of the ServiceNow platform, operating as a method on the GlideRecord object after you've defined your query conditions but executed query(). The method doesn't trigger a new database call—it leverages the result set that's already been fetched and cached by the preceding query() execution.

Under the hood, ServiceNow processes getRowCount() by examining the metadata of your query's result set—the platform tracks how many records were returned when the SQL query executed against the underlying MySQL database. When you call query(), ServiceNow builds and executes a SELECT statement with your WHERE conditions, then maintains a cursor over those results. The getRowCount() method simply returns the record count from that cursor without re-querying the database. This is why you can call getRowCount() multiple times on the same GlideRecord without performance penalties—the count is already known.

Without getRowCount(), you'd be forced to iterate through your entire result set just to count records, which destroys performance and makes your cursor unusable for subsequent iteration. You cannot know in advance whether your while (gr.next()) loop will process 5 records or 5,000 records without this method. This becomes critical when you're implementing pagination, limiting expensive operations based on result size, or providing user feedback about data volume before processing. The method also enables you to make intelligent decisions about whether to proceed with memory-intensive operations or to break large result sets into smaller chunks.

Developers use getRowCount() most frequently in Business Rules and Script Includes when they need to process records but want to understand scope first—like determining whether to send individual notifications or a digest email based on volume. System administrators rely on it in data cleanup scripts to understand the impact before making bulk changes. Enterprise architects use it in migration scripts and integration patterns where they need to process large datasets in batches, using the count to calculate optimal chunk sizes and provide progress indicators.

The method relates directly to GlideAggregate, which you should use instead when you only need a count and won't iterate through records—GlideAggregate generates a SELECT COUNT(*) query rather than fetching full records. It also connects to setLimit() in interesting ways—the count reflects your limit, not the total possible matches, which catches many developers off guard. Finally, it pairs with hasNext() for sophisticated iteration control, where you might need to know both the total count and whether more records remain at your current cursor position.

How It Works Under the Hood

When you call getRowCount() on a GlideRecord, you're accessing metadata that ServiceNow cached during the initial query() execution, not triggering a new database operation. The platform maintains a result set cursor that includes both the actual record data and metadata about the query results, including the total count. This design allows ServiceNow to provide the count instantly while preserving the cursor position for subsequent next() operations.

The critical detail most developers miss is that getRowCount() reflects the exact query that was executed, including any setLimit() restrictions you applied before calling query(). If you limited your query to 100 records but 500 match your criteria, getRowCount() returns 100, not 500. ServiceNow also applies Access Control List (ACL) filtering before counting, so the returned count represents only records the current user can actually access. This server-side security filtering happens transparently but can cause confusion when counts don't match expectations based on direct database queries.

The Request Lifecycle

  1. You build query conditions using addQuery(), addEncodedQuery(), and other filter methods on the GlideRecord object—no database interaction occurs yet.
  2. Calling query() triggers ServiceNow to build a SQL SELECT statement from your conditions, execute it against the MySQL database, and establish a server-side cursor over the results.
  3. ServiceNow applies ACL filtering to the raw database results, removing records the current user cannot access—this filtered set becomes your effective result set.
  4. The platform caches both the filtered record data and metadata including the total count in server memory, positioning the cursor before the first record.
  5. When you call getRowCount(), ServiceNow immediately returns the cached count without database interaction, cursor movement, or additional processing.
  6. Subsequent next() calls iterate through the cached result set, and you can call getRowCount() again at any point to get the same count value.
Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Core Pattern

Business Rule — Incident Assignment.js
// Standard pattern: query first, then get count and iterate
var gr = new GlideRecord('incident');
gr.addQuery('assignment_group', current.getValue('sys_id'));
gr.addQuery('state', 'IN', '1,2,3'); // New, In Progress, On Hold
gr.query();

// Get count before processing - this is instant, no additional DB call
var recordCount = gr.getRowCount();
gs.info('Found ' + recordCount + ' active incidents for assignment group');

// Make processing decisions based on count
if (recordCount > 50) {
    // Use batch processing for large result sets
    processBulkNotification(gr);
} else {
    // Process individual records for smaller sets
    while (gr.next()) {
        sendIndividualNotification(gr);
    }
}
Script Include — NotificationManager.js
var NotificationManager = Class.create();
NotificationManager.prototype = {
    
    processIncidentQueue: function(assignmentGroupId) {
        var gr = new GlideRecord('incident');
        gr.addQuery('assignment_group', assignmentGroupId);
        gr.addQuery('state', '!=', '6'); // Exclude resolved
        gr.query();
        
        // Always check count before expensive operations
        var totalRecords = gr.getRowCount();
        if (totalRecords === 0) {
            gs.info('No incidents to process for group: ' + assignmentGroupId);
            return { processed: 0, skipped: 0 };
        }
        
        return this._processRecords(gr, totalRecords);
    },
    
    type: 'NotificationManager'
};

Real-World Scenarios

Incident Escalation Processing

A scheduled job needs to escalate overdue incidents but wants to adjust its processing strategy based on volume. High-volume days require batch email digests while low-volume days can send individual notifications.

Scheduled Script Execution — Daily Escalation.js
// Find incidents overdue for escalation
var overdueIncidents = new GlideRecord('incident');
overdueIncidents.addQuery('escalation', '0'); // Not yet escalated
overdueIncidents.addQuery('opened_at', '<', gs.daysAgoStart(1));
overdueIncidents.addQuery('state', '!=', '6'); // Not resolved
overdueIncidents.query();

var overdueCount = overdueIncidents.getRowCount();
gs.info('Processing ' + overdueCount + ' overdue incidents');

// Adjust processing strategy based on volume
if (overdueCount > 100) {
    // High volume: send digest to managers only
    var managers = this.getIncidentManagers();
    this.sendDigestEmail(managers, overdueCount);
} else if (overdueCount > 0) {
    // Normal volume: individual escalation emails
    while (overdueIncidents.next()) {
        this.escalateIncident(overdueIncidents);
    }
}

Watch for the zero-count case—many developers forget to handle empty result sets gracefully. Also remember that getRowCount() reflects only records the current user can see, so system context might be necessary for accurate counts. The count remains constant even as you iterate, so you can safely use it for progress calculations throughout the loop.

Catalog Request Approval Routing

A Business Rule determines approval workflow routing based on how many high-value requests a user has submitted in the past month. Single requests go through standard approval while bulk requesters get expedited group approval.

Business Rule — Request Approval Router.js
// Check requester's recent high-value submissions
var recentRequests = new GlideRecord('sc_req_item');
recentRequests.addQuery('requested_for', current.requested_for);
recentRequests.addQuery('opened_at', '>=', gs.daysAgoStart(30));
recentRequests.addQuery('price', '>', 1000); // High-value items only
recentRequests.query();

var requestCount = recentRequests.getRowCount();
gs.info('User has ' + requestCount + ' recent high-value requests');

// Route based on request volume pattern
if (requestCount >= 5) {
    // Bulk requester: expedited group approval
    current.approval = 'requested';
    current.approval_set = this.getBulkApprovalSet();
    gs.info('Routing to bulk approval process');
} else {
    // Standard individual approval process
    current.approval = 'requested';
    current.approval_set = this.getStandardApprovalSet();
}

Be careful with date queries and timezone handling—gs.daysAgoStart() uses system timezone which might not match user expectations. The count here includes cancelled or rejected requests, so add state filtering if you need only successful submissions. Consider that this pattern can create approval inequality if not carefully designed.

Change Management Risk Assessment

A Change Advisory Board script evaluates risk by examining how many changes are scheduled for the same maintenance window. High-density windows trigger additional review processes and stakeholder notifications.

Business Rule — Change Risk Calculator.js
// Find overlapping changes in the same maintenance window
var overlappingChanges = new GlideRecord('change_request');
overlappingChanges.addQuery('start_date', '>=', current.start_date);
overlappingChanges.addQuery('start_date', '<=', current.end_date);
overlappingChanges.addQuery('state', 'IN', '1,2'); // Requested or Approved
overlappingChanges.addQuery('sys_id', '!=', current.sys_id); // Exclude current
overlappingChanges.query();

var conflictCount = overlappingChanges.getRowCount();
gs.info('Found ' + conflictCount + ' overlapping changes in maintenance window');

// Escalate risk based on change density
if (conflictCount >= 3) {
    current.risk = 'high';
    current.justification = 'High change density: ' + conflictCount + ' overlapping changes';
    this.notifyChangeAdvisoryBoard(current, conflictCount);
} else if (conflictCount > 0) {
    current.risk = 'medium';
    this.flagPotentialConflicts(overlappingChanges);
}
⚠️

Date/time queries with overlapping ranges can be tricky—test thoroughly with edge cases like changes starting exactly when others end. The count reflects current state, so approved changes that later get cancelled will still affect historical risk calculations.

The Classic Mistake

⚠️

Using getRowCount() in a loop to conditionally query records instead of building proper query conditions upfront.

Anti-pattern — Do Not Use This.js
// BAD: Multiple queries with getRowCount() checks
function processUserIncidents(userSysId) {
    var highPriority = new GlideRecord('incident');
    highPriority.addQuery('caller_id', userSysId);
    highPriority.addQuery('priority', '1');
    highPriority.query();
    
    if (highPriority.getRowCount() > 0) {
        // Process high priority
        var medPriority = new GlideRecord('incident');
        medPriority.addQuery('caller_id', userSysId);
        medPriority.addQuery('priority', '2');
        medPriority.query();
        
        if (medPriority.getRowCount() > 5) {
            return 'overloaded_user';
        }
    }
    return 'normal_user';
}

This pattern triggers multiple database roundtrips and loads entire result sets into memory just to count them. ServiceNow internally executes a full SELECT * query, instantiates GlideRecord objects for every row, then throws them away to return just the count. You'll see "Transaction cancelled: maximum execution time exceeded" errors in System Log > All when this runs against tables with thousands of records. The browser console shows script timeouts, and sys_admins get alerts about long-running transactions.

The Fix.js
// GOOD: Single optimized query with GlideAggregate
function processUserIncidents(userSysId) {
    var agg = new GlideAggregate('incident');
    agg.addQuery('caller_id', userSysId);
    agg.addQuery('priority', 'IN', '1,2');
    agg.addAggregate('COUNT');
    agg.groupBy('priority');
    agg.query();
    
    var highCount = 0, medCount = 0;
    while (agg.next()) {
        if (agg.priority == '1') highCount = parseInt(agg.getAggregate('COUNT'));
        if (agg.priority == '2') medCount = parseInt(agg.getAggregate('COUNT'));
    }
    
    return (highCount > 0 && medCount > 5) ? 'overloaded_user' : 'normal_user';
}
💡

If you need only a count, never instantiate GlideRecord objects. Use GlideAggregate with COUNT() aggregation for count-only operations.

Performance Rules

  1. Never use getRowCount() on queries returning over 1,000 records — response times exceed 30 seconds and trigger transaction timeouts that crash Business Rules and UI Actions.
  2. Replace getRowCount() with GlideAggregate.addAggregate('COUNT') for count-only operations — eliminates object instantiation overhead and reduces memory consumption by 80%.
  3. Avoid calling getRowCount() in loops or repeated contexts — each call re-executes the full query, causing exponential performance degradation that locks database connections.
  4. Never use getRowCount() on tables without proper indexing on query fields — full table scans on sys_journal_field or sys_audit will crash your instance.
  5. Don't call getRowCount() inside while(gr.next()) loops — creates N+1 query problems that generate thousands of database hits and trigger sys_admin performance alerts.
  6. Cache getRowCount() results in variables when used multiple times — the method doesn't cache internally, so gr.getRowCount() > 0 && gr.getRowCount() < 100 executes the query twice.
  7. Use setLimit(1) with hasNext() instead of getRowCount() > 0 for existence checks — stops after finding the first record rather than counting all matches.
  8. Avoid getRowCount() in client-side scripts over slow connections — the method blocks the UI thread and shows "page unresponsive" dialogs when network latency exceeds 5 seconds.

Side Effects & Platform Behavior

  • Triggers all Before Query Business Rules on the target table, potentially modifying query conditions or causing unexpected side effects through addQuery() calls in BR scripts.
  • Respects Table ACLs and record-level security, returning counts only for records the current user can read — may return different results for different users on the same query.
  • Writes query execution details to sys_db_cache_stats and sys_sql_stats tables for performance monitoring and optimization analysis.
  • Increments database connection pool usage and can exhaust available connections during high-volume operations, causing "no available database connections" errors.
  • Does not trigger Notifications, Workflows, or audit logging since no records are actually read or modified — only counted.
  • Breaks when used with chooseWindow() — always returns the total count across all windows, not the count within the specified window range.
  • Ignores setLimit() restrictions and always counts all matching records, potentially causing performance issues when developers expect limited result sets.
  • Adds entries to the stats.do slow query log when execution time exceeds the configured threshold, alerting administrators to performance problems.
  • Updates session statistics in sys_user_session including query count and execution time, contributing to user activity monitoring and session timeout calculations.
  • Fails silently in scoped applications when accessing tables outside the scope — returns 0 instead of throwing an access violation error.

Debugging When It Breaks

The most common failure is performance degradation manifesting as script timeouts and transaction cancellations. Users see loading spinners that never complete, followed by "The request has timed out" error pages. Developers experience browser console errors like "Uncaught Error: Script timeout" and "NetworkError: 500 Internal Server Error". In Background Scripts or Business Rules, you'll see the dreaded "Transaction cancelled: maximum execution time exceeded" message.

Check System Log > All for entries containing "slow query" or "transaction cancelled" to identify problematic getRowCount() calls. Navigate to System Diagnostics > Stats to examine database query performance and identify tables with excessive query times. The Script Debugger shows execution flow but won't capture the database-level performance issues — you need the SQL stats for that.

Look for log messages containing "GlideRecord query exceeded threshold" followed by table names and row counts. Error patterns include "java.sql.SQLException: Query timeout" and "com.glide.db.DatabaseTimeoutException". Quick diagnostic checklist:

  • Check if query includes indexed fields — run gs.print(gr.getEncodedQuery()) to see actual query conditions
  • Verify table size with SELECT COUNT(*) in System Definition > Tables
  • Test with setLimit(10) to see if query conditions work on small result sets
  • Replace with GlideAggregate and compare execution time in System Log

Quick Reference

  • Returns -1 if query hasn't been executed yet — always call query() first
  • Ignores setLimit() and chooseWindow() restrictions — always counts all matching records
  • Use GlideAggregate.addAggregate('COUNT') for count-only operations — 10x faster performance
  • For existence checks, use setLimit(1) + hasNext() instead of getRowCount() > 0
  • Cache the result in a variable — method doesn't cache internally and re-executes the full query each time
  • Respects ACLs and record-level security — different users may get different counts for identical queries
  • Triggers Before Query Business Rules but not Display, After, or Insert/Update/Delete rules
  • Works client-side via GlideRecord but not GlideRecordSecure — use callback functions for async operations
  • Returns 0 for tables outside application scope instead of throwing access errors
  • Avoid on audit tables (sys_audit, sys_journal_field) without date range filters — causes full table scans