Org Skills

apex-development

Write production-grade Salesforce Apex — bulkified triggers with a trigger framework, service/selector/domain layering, SOQL and DML best practices, governor-limit-aware design, sharing and security (with sharing, USER_MODE, FLS), asynchronous Apex (Queueable, Batch, Schedulable, Platform Events, future), error handling and logging, and Apex test classes that assert behaviour.

Download .zip Raw Source
When agents use itUse this whenever the user asks to write, refactor, review or debug Apex classes, triggers, batch jobs, queueables, schedulers, or unit tests; hits a governor limit (SOQL 101, CPU time, heap, DML rows); asks about trigger recursion, mixed DML, order of execution, or "how do I do X in Apex". Also apply it when reviewing Apex code.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill apex-development -a github-copilot
# or with the org installer (adds .github/skills/apex-development):
npx -y github:AGCO-Global/org-skills add skill apex-development

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill apex-development
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill apex-development -g

Installs the salesforce-apex-skills plugin, which bundles all Salesforce / Apex skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install salesforce-apex-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill apex-development -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill apex-development -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill apex-development -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill apex-development -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Apex Development

Apex runs in a multi-tenant sandbox with hard limits per transaction. Every design choice starts from: "this will be called with 200 records at once, by a user with restricted sharing, inside a transaction that already used half its limits." Write for that case and the single-record case is free.

1. Layering (one class per responsibility)

OrderTriggerHandler      ← trigger delegates here; routes by context, no logic
OrderService             ← business rules, orchestration, DML via Unit of Work
OrderSelector            ← all SOQL for Order__c; USER_MODE; field sets
OrderDomain (optional)   ← record-level validation/defaults (fflib Domain pattern)
OrderController          ← thin @AuraEnabled / @InvocableMethod façade → Service
  • Triggers contain one line: new OrderTriggerHandler().run(); — one trigger per object, always.
  • Use an established trigger framework (fflib, Kevin O'Hara's TriggerHandler, or the org's) with per-object bypass flags and recursion control via static Set<Id> of processed records, not a global boolean.
  • Selectors own SOQL: selectById(Set<Id>), selectOpenByAccount(Set<Id>). Nobody writes SOQL in a service or trigger.
  • Services are static-free instance classes with an interface so they can be mocked (OrderService.newInstance() via a factory / Application class).
  • Cross-cutting: Logger, Constants, Application (factories), UnitOfWork.

2. Bulkification — the non-negotiables

  • Never SOQL or DML inside a loop. Collect Ids → one query into a Map<Id, SObject> → loop in memory → one DML.
  • Aggregate in SOQL (GROUP BY, SUM) rather than in Apex loops when possible.
  • Use Map<Id, List<Child__c>> groupings built in one pass; avoid nested loops over two large lists (O(n²) burns CPU).
  • Trigger context: iterate Trigger.new, compare with Trigger.oldMap.get(id) for change detection; only process records whose relevant fields changed.
  • DML with Database.insert(records, false) when partial success is acceptable; inspect SaveResults and log failures. allOrNone = true (default) when the operation must be atomic.
  • Limits to design against: 100 SOQL / 150 DML statements / 10 000 DML rows / 50 000 query rows / 10 s CPU / 6 MB heap (sync); async doubles most. Check Limits.getQueries() in tests for hot paths.

3. SOQL and DML

  • WITH USER_MODE on every query in user-facing paths; WITH SYSTEM_MODE only in explicitly system-context code with a comment saying why.
  • Select only the fields you use; use Field Sets or a constant field list in the Selector; never SELECT *-style dynamic field dumps.
  • Filter on indexed fields (Id, Name, external Id, lookup, custom indexed); avoid !=, NOT IN, leading % wildcards, and formula fields in WHERE on large tables — they're non-selective and fail over 100k+ rows.
  • Relationship queries (SELECT Id, (SELECT Id FROM Lines__r) FROM Order__c) instead of a second query when the child volume is bounded.
  • FOR UPDATE when you'll mutate records that others may touch concurrently; keep the transaction short.
  • Bind variables always (:accountIds) — no string concatenation into SOQL; use String.escapeSingleQuotes only for unavoidable dynamic SOQL.
  • Dynamic SOQL via Database.queryWithBinds(query, bindMap, AccessLevel.USER_MODE).
  • DML in USER_MODE: Database.insert(records, AccessLevel.USER_MODE) enforces CRUD/FLS; wrap in Unit of Work to order inserts/updates and register relationships.

4. Security

  • Class-level with sharing by default; inherited sharing for utilities; without sharing only in a dedicated, reviewed class with a documented reason.
  • CRUD/FLS via USER_MODE (preferred) or Security.stripInaccessible when you must query in system mode and return to a user.
  • Never trust input from LWC/API: validate types, ranges, ownership (does this user own/see this Id?).
  • No hardcoded Ids, profile names, or record type Ids — use Custom Metadata, Schema.getGlobalDescribe() sparingly (it's expensive; prefer SObjectType.Order__c.getRecordTypeInfosByDeveloperName()).
  • Secrets in Named Credentials / Protected Custom Settings, never in code.

5. Asynchronous Apex — pick correctly

Need Use Notes
Fire-and-forget after commit, small payload Queueable (System.enqueueJob) Chainable, supports complex types; Transaction Finalizer for retry
Process millions of rows Batch Apex (Database.Batchable) 200/scope default; make execute idempotent; Database.Stateful only if needed
Run on a schedule Schedulable → enqueues a Queueable/Batch Keep the execute tiny
Callout from a trigger Queueable (or Platform Event → subscriber) Triggers can't call out synchronously
Decouple producers/consumers, cross-system Platform Events / CDC At-least-once; subscriber trigger must be idempotent; EventBus.publish after commit
Legacy @future Avoid in new code — no chaining, no complex params

Async rules: idempotent by design (re-runs happen), log job Ids, respect the 50 queued jobs limit (chain, don't fan out), and test with Test.startTest()/stopTest() to force execution.

6. Order of execution and pitfalls

  • Know the sequence: before triggers → validation rules → duplicate rules → after triggers → assignment/auto-response → workflow/Flow (before-save Flows run before before-triggers) → escalation → roll-up summary → post-commit (emails, events, async).
  • Mixed DML: setup objects (User, PermissionSet) and non-setup in one transaction → MIXED_DML_OPERATION; move one side to a Queueable or System.runAs in tests.
  • Recursion: after-update triggers re-firing on their own updates — guard with processed-Id sets, and prefer before-context field changes (no extra DML).
  • Trigger.new is read-only in after context; field changes belong in before.
  • Roll-ups: DLRS/Flow for simple; Apex only when volume/logic demands.

7. Error handling and logging

  • Catch specific exceptions (DmlException, QueryException, CalloutException); never empty catch.
  • Rethrow as a domain exception (OrderService.OrderException) or AuraHandledException with a user-safe message at the controller boundary.
  • Log with a proper logger (Nebula Logger or the org's) to a custom object/Platform Event so logs survive rollback; include record Ids, user, quiddity, stack.
  • Database.setSavepoint() / Database.rollback(sp) around multi-step DML that must be atomic across catches.

8. Tests that mean something

  • @IsTest class per class under test; @TestSetup with a TestDataFactory; no SeeAllData=true.
  • Every test: positive, bulk (200+), negative (validation/exception), sharing/runAs where relevant.
  • Test.startTest()/stopTest() to reset limits and flush async; assert on outcomes (Assert.areEqual(expected, actualRecord.Status__c, 'status after approval')) — not just "no exception".
  • Mock callouts with HttpCalloutMock; mock services via interfaces + Stub API (Test.createStub) or a mocking library (ApexMocks).
  • Assert governor usage on hot paths: Assert.isTrue(Limits.getQueries() < 5, 'query count').
  • Aim ≥ 85 % on service/selector classes; the org-wide 75 % minimum is a floor, not a target.

9. Style

  • PMD ruleset in CI (ApexUnitTestClassShouldHaveAsserts, AvoidSoqlInLoops, ApexCRUDViolation, AvoidHardcodingId, cyclomatic complexity).
  • Prettier Apex plugin; PascalCase classes, camelCase members, Test suffix on test classes, Selector/Service/TriggerHandler suffixes.
  • ApexDoc on public methods; small methods (< 40 lines); no God classes.

Anti-patterns to reject

  • SOQL/DML inside loops; SELECT without WHERE on large objects.
  • Logic in triggers; multiple triggers per object; static boolean recursion flags.
  • without sharing on controllers; missing USER_MODE; string-concatenated SOQL.
  • @future in new code; chaining more than a few Queueables without a finalizer.
  • Hardcoded Ids/names; Schema.getGlobalDescribe() in loops.
  • Tests with SeeAllData, no asserts, or single-record-only coverage.
  • Swallowed exceptions; System.debug as the logging strategy.