apex-hardcoded-id-elimination
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).
Best use case
apex-hardcoded-id-elimination is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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).
Teams using apex-hardcoded-id-elimination should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/apex-hardcoded-id-elimination/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How apex-hardcoded-id-elimination Compares
| Feature / Agent | apex-hardcoded-id-elimination | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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).
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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.
---
## Core Concepts
### The four canonical lookup mechanisms
| ID kind | Mechanism | Why |
|---|---|---|
| RecordType | `Schema.SObjectType.X.getRecordTypeInfosByDeveloperName()` | Describe is metadata-driven, no SOQL cost, DeveloperName is stable |
| Profile / Group / Queue / UserRole | `[SELECT Id FROM Profile WHERE Name = :name]` cached per-transaction | DeveloperName / Name is stable; cache avoids SOQL-101 |
| Configurable business-record IDs (default Account, fallback User) | Custom Metadata Type with `Lookup__c` or `Text__c` field | 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.
```apex
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:
```apex
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 metadata deploys with code; the value differs per org and changes without a code release.
### 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.
```apex
// 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
```apex
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
```apex
public static Id queueIdByDevName(String devName) {
return [SELECT Id FROM Group WHERE DeveloperName = :devName AND Type = 'Queue' LIMIT 1].Id;
}
```
Wrap in a static map cache as above.
### Pattern: Custom Metadata-driven config
```apex
RoutingConfig__mdt cfg = RoutingConfig__mdt.getInstance('CaseRouter');
Id defaultQueueId = cfg.DefaultQueue__c; // populated per-org
```
---
## Decision Guidance
| ID is... | Use |
|---|---|
| RecordType | `Schema.SObjectType.X.getRecordTypeInfosByDeveloperName()` |
| Profile / Permission Set | SOQL by `Name` / `DeveloperName`, cached |
| Group / Queue / UserRole | SOQL by `DeveloperName`, cached |
| Environment-specific config (default user, fallback record) | Custom Metadata Type |
| Test seed record | Insert in test, capture `.Id` |
| External-system reference | Custom Metadata or Named Credential, never literal |
---
## Recommended Workflow
1. Grep the codebase for the Salesforce ID regex `[a-zA-Z0-9]{15}(?:[a-zA-Z0-9]{3})?` enclosed in quotes — every hit is a candidate.
2. Classify each hit: RecordType, Profile/Group/Queue, configurable, or test-only.
3. Replace RecordType literals with `Schema...getRecordTypeInfosByDeveloperName()` calls.
4. Replace Profile/Group/Queue literals with cached name-based SOQL helpers.
5. Move configurable IDs to a Custom Metadata Type and read via `getInstance()`.
6. Refactor tests to insert seed records and capture IDs at runtime.
7. Run `scripts/check_apex_hardcoded_id_elimination.py` against the project to confirm zero residual literals outside test setup.
---
## Review Checklist
- [ ] No 15- or 18-char ID literal in non-test Apex
- [ ] All RecordType IDs resolved via `Schema.SObjectType.X.getRecordTypeInfosByDeveloperName()`
- [ ] Profile / Group / Queue lookups cached in a static `Map<String, Id>`
- [ ] Configurable IDs live in a Custom Metadata Type
- [ ] No SOQL for ID lookup inside a loop
- [ ] All Apex variables holding IDs are typed `Id`, not `String`
- [ ] Test classes insert seed data; no hardcoded test IDs
- [ ] Integration payloads source IDs from CMDT or runtime lookup, not literals
---
## Salesforce-Specific Gotchas
1. **15 vs 18 char comparison fails as String.** Stored as `String`, the same record can have two different representations. Always use the `Id` type.
2. **`Name='System Administrator'` is not portable.** Some orgs renamed it; Custom Metadata or `DeveloperName` is safer for any cross-org code.
3. **`getRecordTypeInfosByName` uses translatable label.** Always prefer `getRecordTypeInfosByDeveloperName`.
---
## 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
- `apex/custom-metadata-in-apex` — deployable config types
- `apex/apex-test-data-factory` — test-side ID discipline
- `apex/apex-soql-injection-prevention` — bound variables for name lookupsRelated Skills
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
lwc-imperative-apex
Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).
dataweave-for-apex
Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).
flow-invocable-from-apex
Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).
flow-apex-defined-types
Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.
scheduled-apex-failure-detection-and-monitoring
Use when nightly batch / scheduled Apex jobs are failing without anyone noticing — covers why uncaught exceptions in `execute()` go to the debug log instead of email, how to query `AsyncApexJob` for `Status`, `NumberOfErrors`, and `ExtendedStatus`, when to implement `Database.RaisesPlatformEvents` so the platform publishes `BatchApexErrorEvent` on uncaught failures, how to subscribe to that event with an Apex trigger and notify operators, and how to layer a custom watcher schedule on top so silent-failure modes (job that never started, scheduled class deleted, queue stuck on `Queued`) still surface. Triggers: 'nightly batch failed at 2am with no notification', 'how do we know if a scheduled apex job is failing', 'BatchApexErrorEvent vs custom retry logic', 'Setup Apex Jobs only shows last 7 days, where else can I look', 'job is stuck in queued status nobody noticed for a week'. NOT for general Apex exception handling patterns (use apex/apex-exception-handling-and-logging), NOT for Batch Apex authoring or chunking strategy (use apex/batch-apex-design), NOT for Setup → Apex Jobs UI walkthrough as an admin task (use admin/batch-job-scheduling-and-monitoring), NOT for retry logic itself (use apex/scheduled-apex-retry-patterns once authored).
platform-events-apex
Use when publishing or subscribing to Salesforce Platform Events from Apex, comparing Platform Events with Change Data Capture, or designing event-triggered error handling and monitoring. Triggers: 'EventBus.publish', 'platform event trigger', 'CDC vs Platform Events', 'replay ID', 'high-volume event'. NOT for Flow-only publish/subscribe automation.
health-cloud-apex-extensions
Use this skill when extending Health Cloud via Apex: implementing HealthCloudGA managed-package interfaces, automating care plan lifecycle hooks, processing referrals using Industries Common Components invocable actions, or enforcing HIPAA-compliant logging governance for clinical Apex code. Trigger keywords: CarePlanProcessorCallback, HealthCloudGA namespace, ReferralRequest, ReferralResponse, care plan invocable actions, clinical Apex extension, Health Cloud Apex API. NOT for standard Apex triggers or generic Apex development unrelated to Health Cloud managed-package extension points.
fsl-apex-extensions
Use when writing Apex that calls Field Service Lightning scheduling APIs — AppointmentBookingService, ScheduleService, GradeSlotsService, or OAAS — to book, schedule, grade, or optimize service appointments programmatically. Trigger keywords: FSL Apex namespace, GetSlots, schedule service appointment via code, appointment booking API, FSL optimization API. NOT for standard Apex patterns unrelated to FSL, admin-level scheduling policy configuration, or declarative FSL scheduling.
fsc-apex-extensions
Use this skill when extending Financial Services Cloud (FSC) behavior through Apex: customizing financial rollup recalculation, disabling built-in FSC triggers to write custom trigger logic, implementing Compliant Data Sharing (CDS) participant/role integrations, or building custom FSC action handlers. NOT for standard Apex unrelated to the FSC managed package, standard Salesforce sharing rules, or configuring FSC rollups through the Admin UI — use the admin/financial-account-setup skill for declarative rollup setup.
entitlement-apex-hooks
Use this skill when writing Apex triggers or classes that interact with CaseMilestone records — completing milestones, detecting violations, or reacting to SLA state changes. Trigger keywords: CaseMilestone trigger, auto-complete milestone Apex, milestone violation polling, CompletionDate write pattern. NOT for entitlement process admin setup, milestone configuration in Setup UI, or Flow-based milestone actions.
dynamic-apex
Use when building or reviewing code that constructs SOQL/SOSL at runtime, inspects schema metadata via Schema.describe methods, accesses fields dynamically on sObjects, or performs runtime type inspection. Triggers: 'Database.query', 'Schema.getGlobalDescribe', 'Schema.describeSObjects', 'dynamic field access', 'SObjectType', 'DescribeFieldResult'. NOT for static SOQL queries or query performance tuning — use soql-fundamentals or soql-query-optimization.