apex-switch-on-sobject

Apex switch-on-SObjectType patterns — type dispatching across SObject collections, polymorphic handlers, the typed-variable binding that makes `when SObjectType varName` more than a tag check. Covers the single-type-per-when-block restriction, the null branch, the no-fall-through rule, and why `Type.forName()` cannot be used in a switch expression. NOT for basic Apex switch syntax on Integer / String / enum (use Apex Developer Guide directly), NOT for dynamic field access (see apex/dynamic-apex).

Best use case

apex-switch-on-sobject is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Apex switch-on-SObjectType patterns — type dispatching across SObject collections, polymorphic handlers, the typed-variable binding that makes `when SObjectType varName` more than a tag check. Covers the single-type-per-when-block restriction, the null branch, the no-fall-through rule, and why `Type.forName()` cannot be used in a switch expression. NOT for basic Apex switch syntax on Integer / String / enum (use Apex Developer Guide directly), NOT for dynamic field access (see apex/dynamic-apex).

Teams using apex-switch-on-sobject 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

$curl -o ~/.claude/skills/apex-switch-on-sobject/SKILL.md --create-dirs "https://raw.githubusercontent.com/PranavNagrecha/AwesomeSalesforceSkills/main/skills/apex/apex-switch-on-sobject/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/apex-switch-on-sobject/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How apex-switch-on-sobject Compares

Feature / Agentapex-switch-on-sobjectStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Apex switch-on-SObjectType patterns — type dispatching across SObject collections, polymorphic handlers, the typed-variable binding that makes `when SObjectType varName` more than a tag check. Covers the single-type-per-when-block restriction, the null branch, the no-fall-through rule, and why `Type.forName()` cannot be used in a switch expression. NOT for basic Apex switch syntax on Integer / String / enum (use Apex Developer Guide directly), NOT for dynamic field access (see apex/dynamic-apex).

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 Switch on SObject

`switch on` over an SObject expression is the right tool for type-dispatch
inside a heterogeneous collection — a generic trigger handler, a polymorphic
processor, a classifier that runs different validation per object type. It
buys two things over a chain of `instanceof` / `getSObjectType().equals(...)`
checks: **type-narrowed bindings inside each branch** (no cast required) and
a single explicit `when else` for the unhandled case.

What this skill is NOT. The simpler `switch on Integer / String / enum` form
is plain Apex syntax — go directly to the Apex Developer Guide. Dynamic field
access (`Schema.getGlobalDescribe()`, `record.get(fieldName)`) lives in
`apex/dynamic-apex`. This skill is specifically about the SObject expression
form and the branch-binding semantics.

---

## Before Starting

- Confirm the dispatch is *runtime* — if you statically know the type at
  compile time, skip the switch and just write the typed code.
- Confirm whether **`when else`** is required: if the switch is non-exhaustive
  (you only handle some types), you almost always need `when else` because
  unmatched cases silently fall through to the end of the switch with no
  side-effect — that's the failure mode that hides bugs longest.
- Note that **the binding form requires exactly one SObject type per `when`
  block**. You cannot write `when Account, Contact a { ... }` and expect a
  typed `a`; that form is reserved for value-list matching on enums and
  primitives.

---

## Core Concepts

### The two switch-on-SObject forms

```apex
// Form 1 — bind a typed variable inside the branch (the whole point of this feature).
switch on record {
    when Account a {
        // 'a' is typed Account here. No cast, no Schema check.
        a.AccountNumber = generateAccountNumber();
    }
    when Contact c {
        c.LastName = c.LastName?.toUpperCase();
    }
    when null {
        // Reached only when 'record' itself is null — safe, no NullPointerException.
        return;
    }
    when else {
        // Anything else; the runtime type is something we didn't handle.
        // SILENTLY SKIPPING this branch is the #1 latent bug — declare it explicitly.
        System.debug('Unhandled SObjectType: ' + record.getSObjectType());
    }
}
```

```apex
// Form 2 — match by exact value (rare for SObjects; mostly for primitives).
switch on someInteger {
    when 1, 2, 3 { /* multi-value matching only works for primitives + enums */ }
    when else    { ... }
}
```

### Three rules that bite people

1. **No fall-through.** Each `when` branch executes and the switch exits. There is no `break` keyword and no implicit fall-through to the next branch (unlike C / Java's switch).
2. **`when null` matches a null expression.** It does NOT match a record whose Id is null. It is reached only when the *expression* itself is null. This makes switch null-safe by design — no `NullPointerException` from the dispatch.
3. **`Type.forName(...)` is NOT a substitute for SObjectType.** `Type.forName('Account')` returns `System.Type`, which is not what `switch on` accepts in this form. To dispatch from a string name, look up the SObject prototype: `Schema.getGlobalDescribe().get(name).newSObject()` and switch on *that*.

---

## Common Patterns

### Pattern A — Polymorphic trigger handler

**When to use.** A single handler that runs across multiple SObject types
(common in framework code, `templates/apex/TriggerHandler.cls`).

```apex
public class PolyHandler {
    public static void handle(List<SObject> records) {
        for (SObject record : records) {
            switch on record {
                when Account a   { handleAccount(a); }
                when Contact c   { handleContact(c); }
                when Lead l      { handleLead(l); }
                when else        {
                    // Programmer error — every type the framework registers must have a branch.
                    throw new HandlerException('Unhandled type: ' + record.getSObjectType());
                }
            }
        }
    }
}
```

**Why not the alternative.** A chain of
`if (record instanceof Account) { Account a = (Account) record; ... }` is
verbose and re-introduces casts that the binding form already handled.

### Pattern B — Classifier with explicit unhandled-case

**When to use.** You handle some types and want all other types to fall
through harmlessly with a logged warning.

```apex
public static String classify(SObject record) {
    switch on record {
        when Account a   { return a.Type;            }
        when Contact c   { return c.LeadSource;      }
        when null        { return null;              }
        when else        {
            // Crucial: without this branch, the method returns the default null
            // and the caller has no signal that 'record' was an unrecognized type.
            ApplicationLogger.warn('classify: unhandled ' + record.getSObjectType());
            return null;
        }
    }
}
```

### Pattern C — Dispatch from a string type name

**When to use.** The caller has the type name as a string (config-driven
dispatch, custom-metadata-driven router).

```apex
public static SObject createByName(String objectApiName) {
    Schema.SObjectType t = Schema.getGlobalDescribe().get(objectApiName);
    if (t == null) {
        throw new IllegalArgumentException('Unknown SObject: ' + objectApiName);
    }
    SObject prototype = t.newSObject();
    switch on prototype {
        when Account a   { return new Account(Name = 'Default Name');     }
        when Contact c   { return new Contact(LastName = 'Default Last'); }
        when else        {
            // The string named a real SObject, but this method only constructs a few.
            throw new IllegalArgumentException('Unsupported for default-create: ' + objectApiName);
        }
    }
}
```

`Type.forName(objectApiName)` does NOT work as the switch expression here —
it returns `System.Type`, not an SObject prototype.

---

## Decision Guidance

| Situation | Use this | Reason |
|---|---|---|
| Trigger handler dispatching across 3+ SObject types | `switch on record` with one branch per type | Typed bindings, explicit `when else`, no casts |
| Single-type check ("is this an Account?") | `record.getSObjectType() == Account.SObjectType` | Switch overhead not worth it for one check |
| Dispatch from a string SObject API name | `Schema.getGlobalDescribe().get(name).newSObject()` then switch on the prototype | `Type.forName()` returns `System.Type`, not SObjectType |
| Heterogeneous list with frequent unhandled types | Switch with `when else` returning early or logging | Silent-skip is the failure mode without `when else` |
| Two types share identical handling | Two `when` branches calling the same private method | Multi-type-per-when binding is not supported |

---

## Recommended Workflow

1. **Confirm runtime dispatch.** If the type is known at compile time, write typed code; do not introduce a switch.
2. **List every SObject type the dispatch can see.** From the caller, the trigger registration, or the framework contract.
3. **Choose `when else` semantics.** Either (a) throw — programmer error, the dispatch should be exhaustive — or (b) log + return safely. Never omit `when else` on a non-exhaustive switch.
4. **Place `when null` before `when else`** if the expression can be null. The `when null` branch is the null-safety guarantee for the entire switch.
5. **Verify in test.** Write a test class that passes one record of every handled type plus one record of an unhandled type; assert the unhandled-case behavior matches step 3.

---

## Review Checklist

- [ ] Switch has either `when else` or every concrete SObject type the caller can pass.
- [ ] `when null` branch present if the expression can be null.
- [ ] No `when else` block silently returns / does nothing without a comment justifying it.
- [ ] No use of `Type.forName(...)` as the switch expression.
- [ ] Test class covers one record of every handled type + one unhandled type.
- [ ] Each branch's bound variable is used (or removed); unused-binding is a smell that the branch could be just `when Account` without binding.

---

## Salesforce-Specific Gotchas

1. **Silent skip without `when else`.** A non-exhaustive switch on an unhandled type does nothing — no exception, no warning. Always declare `when else`. (See `references/gotchas.md` § 1.)
2. **`Type.forName('Account')` does not produce a switch-able expression.** It returns `System.Type`. You need `Schema.getGlobalDescribe().get('Account').newSObject()` to get an SObject prototype. (See `references/gotchas.md` § 2.)
3. **Multi-type-per-when is not supported.** `when Account a, Contact c { ... }` does not compile. (See `references/gotchas.md` § 3.)
4. **`when null` matches the expression being null, not an SObject with `Id = null`.** Don't use it for "new record" detection. (See `references/gotchas.md` § 4.)
5. **Managed-package custom SObjects need the namespace.** `when my_pkg__Custom_Object__c c { ... }` — the namespace is part of the type identity. (See `references/gotchas.md` § 5.)

---

## Output Artifacts

| Artifact | Description |
|---|---|
| Refactored Apex | Switch-based dispatch replacing instanceof chains, with typed bindings per branch |
| Test class | One record per handled type + one unhandled type; asserts `when else` behavior |
| `when else` decision rationale | One sentence per refactor: "throw" or "log + safe-return" |

---

## Related Skills

- `apex/dynamic-apex` — Schema.getGlobalDescribe / dynamic field access; pair with this skill when dispatching from a string API name
- `apex/trigger-framework` — when this dispatch goes inside a generic trigger handler; `templates/apex/TriggerHandler.cls` is the canonical base
- `apex/apex-mocking-and-stubs` — for the test class that exercises every branch

Related Skills

apex-managed-sharing-patterns

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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

8
from PranavNagrecha/AwesomeSalesforceSkills

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.

feature-flags-and-kill-switches

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when implementing runtime feature toggles, emergency kill switches, or gradual rollout controls in Apex using Custom Metadata Types, Custom Permissions, or Hierarchical Custom Settings. NOT for Custom Metadata Type fundamentals — see custom-metadata-types skill for CMDT basics.

entitlement-apex-hooks

8
from PranavNagrecha/AwesomeSalesforceSkills

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.