Stop selecting the offending field. For dynamic SOQL, gate every selected field through Schema.DescribeFieldResult.isQueryable(). For Tooling API queries, replace Name/RecordName with the queryable equivalent — usually DeveloperName, MasterLabel, or QualifiedApiName.
What "is not marked queryable" actually means
Every field in Salesforce has a set of metadata flags describing what you're allowed to do with it. One of those flags is IsQueryable on the field's DescribeFieldResult. When it's false, Salesforce will refuse the query — even if your user has full read access — because the field is internal, computed outside the query engine, or only exposed through the describe layer.
RecordName is the specific case most developers hit. It appears as a field on several metadata-flavoured objects (the Tooling API surface, history tables, certain relationship lookups), but you cannot SELECT RecordName directly. Salesforce expects you to ask for the underlying record's name via a relationship traversal, or to use the queryable equivalent for that object.
The three causes — and the fix for each
1. Selecting RecordName from a history or share table
History tables (AccountHistory, custom __History) and share tables expose the parent record via a polymorphic relationship, not a direct field. Selecting RecordName trips the error. Traverse the relationship instead:
// Fails: field 'recordname' is not marked queryable
List<AccountHistory> bad = [SELECT Id, RecordName FROM AccountHistory];
// Works: traverse the parent relationship
List<AccountHistory> good = [
SELECT Id, Account.Name, Field, OldValue, NewValue
FROM AccountHistory
];2. Tooling API queries on metadata objects
Objects like CustomField, EntityDefinition, and ApexClass expose Name only in describe results — not via SOQL. Use the queryable surrogate:
DeveloperName— the API name without the namespace prefix.MasterLabel— the display label, regardless of user locale.QualifiedApiName— fully qualified name including namespace.
// Fails on Tooling API
[SELECT Id, RecordName FROM CustomField WHERE TableEnumOrId = 'Account']
// Works
[SELECT Id, DeveloperName, QualifiedApiName, TableEnumOrId
FROM CustomField
WHERE TableEnumOrId = 'Account']3. Dynamic SOQL built from Schema.getGlobalDescribe()
The most insidious case. You loop over every field in a describe, build a comma-separated select list, and a single non-queryable field (compound address fields, encrypted fields, external lookup fields, formula reference fields) takes the entire query down. Filter before you concatenate:
Schema.DescribeSObjectResult d = Account.SObjectType.getDescribe();
List<String> fields = new List<String>();
for (Schema.SObjectField f : d.fields.getMap().values()) {
Schema.DescribeFieldResult info = f.getDescribe();
// Guard every dynamic select. isAccessible() alone is not enough —
// a field can be accessible-via-describe but not queryable.
if (info.isQueryable() && info.isAccessible()) {
fields.add(info.getName());
}
}
String soql = 'SELECT ' + String.join(fields, ', ') + ' FROM Account LIMIT 10';
List<SObject> rows = Database.query(soql);isQueryable() is what fixes this category, not isAccessible() or isFilterable(). They check different things; mixing them up is a common cause of "I gated my query but it still fails."
How to find the offending field fast
The error message in Apex doesn't always tell you which field is the problem when you're selecting many. Two strategies:
- Binary-search the select list. Comment out half the fields, re-run, keep halving until you isolate the field. Brutal but fast.
- Pre-flight with describe. Before running the query, log every field whose
isQueryable()isfalse:
for (Schema.SObjectField f : Account.SObjectType.getDescribe().fields.getMap().values()) {
Schema.DescribeFieldResult info = f.getDescribe();
if (!info.isQueryable()) {
System.debug('Not queryable: ' + info.getName() + ' (' + info.getType() + ')');
}
}Reproduce and fix it locally — no org round-trip
Iterating on dynamic SOQL against a real org means a deploy-run-wait cycle for every edit. Nimbus runs the same Apex against an embedded PostgreSQL with your project's schema loaded, so you can rebuild the query, re-run the describe loop, and check isQueryable() output in under a second. The fix lands before you ever push to the org.
# In your SFDX project
nimbus test "MyDynamicQueryTest.*"
# Or run anonymous Apex with the describe loop above
nimbus run-anonymous scripts/check-queryable.apex