Salesforce Tooling API Queries Directly in Apex

Salesforce Tooling API Queries Directly in Apex

Step-by-Step Guide

As Salesforce developers, architects, and admins, we often find ourselves needing to query metadata that standard SOQL just can’t touch. Whether you are building custom dev tools, auditing ApexClass versions, or analyzing validation rules, the Salesforce Tooling API is your best friend.

While many use the Tooling API via external tools like Postman or the Developer Console, executing a /tooling/query directly within Apex unlocks powerful automation capabilities right inside your org.

In this guide, we will walk through how to build and execute a Tooling API query using Apex, break down the code step-by-step, and cover essential best practices for production-ready implementations.

Note:

Add your Salesforce My Domain URL in Remote Site Settings.

Why Use /tooling/query via Apex?

Standard SOQL is built for data objects (like Accounts and Contacts). The Tooling API, however, is designed for metadata and development artifacts.

By executing Tooling queries natively in Apex, you can:

  • Automate code quality and metadata audits.
  • Dynamically fetch metadata properties (e.g., Apex coverage, custom field details).
  • Build custom administrative dashboards directly inside Lightning Web Components (LWCs).

Step-by-Step Code Walkthrough

To query the Tooling API from Apex, you need to make an internal HTTP callout. Here is the foundational pattern to achieve this:

// 1. Define your Tooling SOQL Query
String soqlQuery = 'SELECT Id, Name, ApiVersion, Status FROM ApexClass WHERE Status = \'Active\'';

// 2. URL encode the query text to handle spaces and special characters
String encodedQuery = EncodingUtil.urlEncode(soqlQuery, 'UTF-8');

// 3. Build the Tooling API Endpoint URL
String baseUrl = URL.getOrgDomainUrl().toExternalForm();
String endpoint = baseUrl + '/services/data/v66.0/tooling/query/?q=' + encodedQuery;

// 4. Initialize and configure the HTTP Request
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('GET');

// Use a Named Credential or a secure Session ID approach for Auth
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');

// 5. Send the request
Http http = new Http();
try {
    HttpResponse res = http.send(req);
    if (res.getStatusCode() == 200) {
        System.debug(res.getBody()); // Returns raw JSON response
    } else {
        System.debug('Error Status: ' + res.getStatus() + ' Code: ' + res.getStatusCode());
        System.debug('Error: ' + res.getBody());
    }
} catch(Exception e) {
    System.debug('Exception thrown: ' + e.getMessage());
    System.debug('Exception: ' + e.getMessage());
}

Breaking Down the Implementation

  1. The Tooling Query: We define a query targeting ApexClass. Notice that we are looking for metadata attributes (ApiVersion, Status) that standard SOQL cannot filter effectively in the same way.
  2. URL Encoding: Since standard SOQL contains spaces and single quotes, EncodingUtil.urlEncode() ensures the query is safely formatted as a URL parameter.
  3. Endpoint Construction: The endpoint targets the /tooling/query/ REST service. It dynamically sniffs the org’s base URL using URL.getOrgDomainUrl().toExternalForm().
  4. Authentication & Headers: An HTTP GET request is initialized. The Authorization header utilizes the current user’s session ID to authenticate the internal callout.
  5. The Callout & Exception Handling: The request is wrapped in a try-catch block to gracefully capture network errors, while an if-else statement validates that the HTTP status code returns a successful 200 OK.

Best Practices & Recommendations for Production

While the provided code snippet is an excellent functional foundation, deploying Tooling API calls in a production environment requires a few strategic optimizations:

1. Shift to Named Credentials

  • The Issue: UserInfo.getSessionId() is notoriously unreliable in asynchronous Apex (like Queueables, Batch Apex, or Schedulers) and may return null or trigger authorization errors.
  • The Fix: Set up a Named Credential that connects back to your own Salesforce org via OAuth. Replace the hardcoded baseUrl and Authorization header with the Named Credential endpoint (e.g., req.setEndpoint('callout:MySalesforceOrg/services/data/v66.0/tooling/query/?q=' + encodedQuery);). This securely offloads authentication to the platform.

2. Dynamically Manage the API Version

  • The Issue: The endpoint hardcodes /v66.0/. While functional, hardcoding API versions can lead to technical debt when older versions are eventually deprecated by Salesforce.
  • The Fix: Consider extracting the version to a Custom Metadata Type or dynamically constructing it to match your org’s current release cadence.

3. Handle JSON Deserialization

  • The Issue: res.getBody() returns a raw JSON string.
  • The Fix: To actually use this data in your Apex logic or pass it to an LWC, create a typed Apex wrapper class or use JSON.deserializeUntyped(res.getBody()) to parse the payload into usable objects or maps.

4. Remember Callout Governor Limits

  • The Issue: This script counts as an HTTP callout.
  • The Fix: Ensure this logic does not execute after Data Manipulation Language (DML) operations in the same transaction, or you will encounter the dreaded System.CalloutException: You have uncommitted work pending error.

Summary

Querying the Tooling API natively in Apex bridges the gap between metadata administration and programmatic automation. By following this pattern—and upgrading it with Named Credentials and robust JSON parsing—you can build seamless dev-tooling automation directly inside Salesforce.

Leave a Reply