Remove hardcoded Salesforce record IDs (Profile, RecordType, User, Queue, custom) from Apex and replace with describe-API, name-based SOQL, or Custom Metadata-driven lookups. NOT for storing config data — see apex-custom-settings-hierarchy / custom-metadata-in-apex. NOT for ID-based sharing rules (see sharing-selection).
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Remove hardcoded Salesforce record IDs (Profile, RecordType, User, Queue, custom) from Apex and replace with describe-API, name-based SOQL, or Custom Metadata-driven lookups. NOT for storing config data — see apex-custom-settings-hierarchy / custom-metadata-in-apex. NOT for ID-based sharing rules (see sharing-selection).
["how to remove hardcoded profile id from apex","record type id different in sandbox vs production","apex breaks after sandbox refresh because of hardcoded ids","replace hardcoded queue id with developer name lookup","schema getrecordtypeinfosbydevelopername best practice","15 char vs 18 char id comparison failing in apex","use metadata relationship field instead of hardcoding record id","does getinstance query soql or use the application cache"]
inputs
["Apex class or trigger containing literal 15/18-char IDs","Org context (sandbox, scratch, prod) where IDs differ","Required RecordType / Profile / Queue / Group developer names","Custom Metadata Type for config-driven IDs (if applicable)"]
outputs
["Refactored Apex using describe-API or name-based lookup","Cached helper class for Profile / Queue / Group ID resolution","Custom Metadata mapping for config-driven IDs","Test class that inserts data instead of hardcoding IDs"]
dependencies
[]
version
1.1.0
author
Pranav Nagrecha
updated
"2026-07-07T00:00:00.000Z"
Apex Hardcoded ID Elimination
Activate when Apex contains literal Salesforce record IDs ('00e1x000000ABcD', '012xx0000004C9I', '00G3x000003abcD'). Hardcoded IDs are catastrophic in any multi-org topology: the same Profile, RecordType, Queue, or User has a different ID in sandbox vs production, in every scratch org, and after some sandbox refreshes. Code that runs in prod silently breaks the moment it deploys to a sandbox copy, a partial copy refresh, or a scratch org spun up for CI.
The fix is to look IDs up by something stable — DeveloperName, MasterLabel via describe, or a Custom Metadata mapping — and to cache the result for the rest of the transaction.
Before Starting
Identify the ID kind. RecordType IDs come from the describe API. Profile, Group, Queue, UserRole IDs come from SOQL by DeveloperName. Configurable IDs (a default Account, a routing User) belong in Custom Metadata.
Confirm DeveloperName, not Name. "System Administrator" has been renamed to "Standard System Administrator" in some orgs; DeveloperName (SysAdmin) is the API-stable identifier. For Profile, the canonical predicate is Name, but the value differs across org versions — Custom Metadata is safer for any cross-org code.
Audit tests. A test class that hardcodes a sandbox-specific ID will not run in scratch orgs or new sandboxes.
Custom Metadata Type with a Text__c field holding the Id
Subscriber-org safe, deployable, no code change to retarget
Test data IDs
insert then capture record.Id
Test data is created per run; never persistent
Schema describe for RecordType
Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName() returns Map<String, Schema.RecordTypeInfo>. Always use DeveloperName, never MasterLabel — labels are translatable.
Id customerRtId = Schema.SObjectType.Account
.getRecordTypeInfosByDeveloperName()
.get('Customer')
.getRecordTypeId();
This is metadata-driven, costs no SOQL, and works across every org.
Caching SOQL-derived IDs
Profile/Group/Queue lookups are SOQL. A single naive lookup inside a loop blows the SOQL-101 limit. Cache once per transaction:
private static Map<String, Id> profileIdByName;
public static Id getProfileId(String name) {
if (profileIdByName == null) {
profileIdByName = new Map<String, Id>();
for (Profile p : [SELECT Id, Name FROM Profile]) {
profileIdByName.put(p.Name, p.Id);
}
}
return profileIdByName.get(name);
}
The static map lives for the transaction. Subsequent calls are free.
Custom Metadata for configurable IDs
When the "right" ID is environment-specific (a default-owner User, a routing Queue, a fallback Account), put the mapping in a Custom Metadata Type. The records deploy through the Metadata API as declarative XML components inside packages and change sets — they travel as metadata artifacts, not database data rows — so the value differs per org and changes without a code release.
Prefer a Metadata Relationship field over a raw Text__c ID. When the thing you're referencing is itself metadata — another custom metadata record, an object (EntityDefinition), a field (FieldDefinition), or an entity particle — Salesforce provides a purpose-built Metadata Relationship field type. It stores a stable reference and enforces referential integrity instead of parking a raw 15/18-char ID in a text field, which is exactly the failure mode this skill exists to remove. Custom metadata types have no Lookup relationship field type, and Master-detail is not offered either; Metadata Relationship is the only supported relationship field. Reserve a plain Text__c field (holding the record Id) for referencing data records (a specific Account or User) that metadata relationships can't target — a Text field is the only way to store a data-record Id on a custom metadata type.
getInstance() / getAll() are SOQL-free. These static methods read from the application cache, not the database, so they cost no SOQL query and no query-row governor limits — a material reason to prefer CMDT over the cached-SOQL Map pattern used above for Profile/Group/Queue. getAll() returns a Map<String, T__mdt> keyed by DeveloperName; getInstance(...) returns one record by DeveloperName, record Id, or qualified API name.
Watch the 255-character truncation.getInstance()/getAll() return only the first 255 characters of every field. If a config value (a long endpoint, a serialized payload) exceeds 255 chars, the cached read silently truncates it — fall back to a full SOQL query against the __mdt type to get the complete value.
Seeding and visibility. Apex can create, read, and update custom metadata records but cannot delete them, and DML on custom metadata is not allowed through the Partner or Enterprise APIs — plan CMDT seeding scripts accordingly. Type visibility (Public, Protected, PackageProtected) controls which namespaces can see the type and its records: use Protected/PackageProtected when a managed package must hide environment-specific config such as API keys from subscriber-org Apex.
Test class discipline
Tests must never hardcode any record ID. Always insert seed data and capture the resulting ID. Test.startTest() / Test.stopTest() and TestDataFactory patterns belong here — see templates/apex/tests/TestDataFactory.cls.
Id vs String — the 15/18-char trap
Salesforce IDs are 15 chars (case-sensitive) or 18 chars (case-insensitive). Stored as String, the same record yields two different values that fail equality. Always use the Id data type.Id normalizes to 18-char internally; equality works.
// WRONG
String accId = '0011x00000ABCDe'; // 15-char
if (accId == acc.Id) { ... } // acc.Id is 18-char — never matches
// RIGHT
Id accId = '0011x00000ABCDe'; // auto-normalized to 18
if (accId == acc.Id) { ... } // works
Integration boundary
When a third-party system requires a Salesforce ID (webhook target, named credential payload), that ID must come from a name-based lookup or Custom Metadata at runtime, never a literal in code. A literal pinned to one org silently sends the wrong ID after a refresh.
Common Patterns
Pattern: RecordType resolution helper
public with sharing class RecordTypes {
private static final Map<String, Map<String, Id>> CACHE = new Map<String, Map<String, Id>>();
public static Id idFor(SObjectType sot, String developerName) {
String key = String.valueOf(sot);
if (!CACHE.containsKey(key)) {
Map<String, Id> byName = new Map<String, Id>();
for (Schema.RecordTypeInfo rti :
sot.getDescribe().getRecordTypeInfosByDeveloperName().values()) {
byName.put(rti.getDeveloperName(), rti.getRecordTypeId());
}
CACHE.put(key, byName);
}
return CACHE.get(key).get(developerName);
}
}
Pattern: Group / Queue lookup by DeveloperName
public static Id queueIdByDevName(String devName) {
return [SELECT Id FROM Group WHERE DeveloperName = :devName AND Type = 'Queue' LIMIT 1].Id;
}
getInstance()/getAll() truncate fields to 255 chars. They read the application cache, not the row. Any config value longer than 255 characters comes back truncated — query the __mdt type with SOQL to get the full value.
Apex can't delete custom metadata records. Create/read/update only; DML on CMDT is also blocked through the Partner and Enterprise APIs. Don't design a seeding routine that expects to delete CMDT rows.
Output Artifacts
Artifact
Description
Refactored Apex class
All literal IDs replaced with describe / SOQL / CMDT lookups
Cached lookup helper
RecordTypes, Profiles, Queues helpers with static maps
Custom Metadata Type
Config-driven mapping for environment-specific IDs
Refactored test class
Inserts seed data; captures IDs at runtime
Related Skills
apex/apex-custom-settings-hierarchy — when config differs per profile/user