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
- 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. - URL Encoding: Since standard SOQL contains spaces and single quotes,
EncodingUtil.urlEncode()ensures the query is safely formatted as a URL parameter. - Endpoint Construction: The endpoint targets the
/tooling/query/REST service. It dynamically sniffs the org’s base URL usingURL.getOrgDomainUrl().toExternalForm(). - Authentication & Headers: An HTTP GET request is initialized. The
Authorizationheader utilizes the current user’s session ID to authenticate the internal callout. - The Callout & Exception Handling: The request is wrapped in a
try-catchblock to gracefully capture network errors, while anif-elsestatement validates that the HTTP status code returns a successful200 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 returnnullor trigger authorization errors. - The Fix: Set up a Named Credential that connects back to your own Salesforce org via OAuth. Replace the hardcoded
baseUrlandAuthorizationheader 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 pendingerror.
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.