apex-stripinaccessible-and-fls-enforcement

Use Security.stripInaccessible to enforce CRUD/FLS on user-supplied records before DML, and to scrub query results before returning them to clients. Covers AccessType.READABLE/CREATABLE/UPDATABLE/UPSERTABLE, the SObjectAccessDecision API, when to prefer WITH USER_MODE on the SOQL itself, and integration with the SecurityUtils template. NOT for class-level sharing keyword choice (with sharing / without sharing / inherited sharing — see apex-sharing-keywords). NOT for managed sharing or Apex managed sharing recalculations (see sharing-selection decision tree).

Best use case

apex-stripinaccessible-and-fls-enforcement is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Use Security.stripInaccessible to enforce CRUD/FLS on user-supplied records before DML, and to scrub query results before returning them to clients. Covers AccessType.READABLE/CREATABLE/UPDATABLE/UPSERTABLE, the SObjectAccessDecision API, when to prefer WITH USER_MODE on the SOQL itself, and integration with the SecurityUtils template. NOT for class-level sharing keyword choice (with sharing / without sharing / inherited sharing — see apex-sharing-keywords). NOT for managed sharing or Apex managed sharing recalculations (see sharing-selection decision tree).

Teams using apex-stripinaccessible-and-fls-enforcement 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-stripinaccessible-and-fls-enforcement/SKILL.md --create-dirs "https://raw.githubusercontent.com/PranavNagrecha/AwesomeSalesforceSkills/main/skills/apex/apex-stripinaccessible-and-fls-enforcement/SKILL.md"

Manual Installation

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

How apex-stripinaccessible-and-fls-enforcement Compares

Feature / Agentapex-stripinaccessible-and-fls-enforcementStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Use Security.stripInaccessible to enforce CRUD/FLS on user-supplied records before DML, and to scrub query results before returning them to clients. Covers AccessType.READABLE/CREATABLE/UPDATABLE/UPSERTABLE, the SObjectAccessDecision API, when to prefer WITH USER_MODE on the SOQL itself, and integration with the SecurityUtils template. NOT for class-level sharing keyword choice (with sharing / without sharing / inherited sharing — see apex-sharing-keywords). NOT for managed sharing or Apex managed sharing recalculations (see sharing-selection decision tree).

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 stripInaccessible and FLS Enforcement

Activate when Apex code accepts records from a less-privileged context (LWC, Aura, REST, Visualforce) and writes them to the database, OR when query results need to be scrubbed of fields the running user cannot read. `Security.stripInaccessible(AccessType, records)` removes inaccessible fields from a record collection and returns an `SObjectAccessDecision` you must operate on instead of the original list.

---

## Before Starting

- Confirm the records are user-supplied. Internal trusted-context data (e.g., trigger handler reading from the trigger context with `without sharing`) does not need stripInaccessible.
- Decide the AccessType: are you reading, creating, updating, or upserting? The wrong enum will silently strip the wrong fields.
- Confirm the calling class's sharing keyword. `with sharing` enforces record visibility; stripInaccessible enforces FIELD visibility. Both are needed for full enforcement.
- If the SOQL itself is fetching records to return to a less-privileged caller, prefer `WITH USER_MODE` (Summer '23+) on the query and skip a redundant strip pass.

---

## Core Concepts

### The four AccessType enum values

`AccessType` is the first argument to `Security.stripInaccessible`. Each value evaluates a different permission:

| Enum | What it strips | When to use |
|---|---|---|
| `AccessType.READABLE` | Fields the user cannot read. | AFTER a SOQL query whose results you'll return to a less-privileged caller. |
| `AccessType.CREATABLE` | Fields the user cannot create. | BEFORE `insert`. |
| `AccessType.UPDATABLE` | Fields the user cannot update. | BEFORE `update`. |
| `AccessType.UPSERTABLE` | Fields the user cannot create OR update (strict intersection — a field must be both creatable AND updatable to survive). | BEFORE `upsert`. |

Picking the wrong AccessType is a real bug. `READABLE` before an `update` will leave create-only or update-only fields in the payload that the user could not have set themselves.

### SObjectAccessDecision — getRecords, getRemovedFields, getModifiedRecords

`Security.stripInaccessible(...)` returns an immutable `SObjectAccessDecision`. You cannot mutate the original list and call it safe — you must operate on the decision's outputs:

- **`getRecords()`** — the sanitized list with inaccessible fields removed. Always DML on this, never on the original argument.
- **`getRemovedFields()`** — a `Map<String, Set<String>>` keyed by SObject API name, value is the set of removed field API names. Useful for logging/audit and for surfacing soft warnings.
- **`getModifiedRecords()`** — a `Map<Id, SObject>` of records that had at least one field stripped (post-DML id only — useful when re-fetching).

### Canonical pattern wrapping DML

```
public void createCases(List<Case> userSupplied) {
    SObjectAccessDecision decision =
        Security.stripInaccessible(AccessType.CREATABLE, userSupplied);

    if (!decision.getRemovedFields().isEmpty()) {
        ApplicationLogger.warn('createCases',
            JSON.serialize(decision.getRemovedFields()));
    }

    insert decision.getRecords();   // NEVER `insert userSupplied;`
}
```

The single most common bug is calling `insert userSupplied;` after the strip — the strip then has zero effect.

### When stripInaccessible is the right choice vs WITH USER_MODE

| Goal | Prefer |
|---|---|
| Querying records to return/manipulate as the running user | `WITH USER_MODE` on the SOQL itself |
| DML on records assembled from user input (e.g., REST body) | `stripInaccessible(CREATABLE/UPDATABLE/UPSERTABLE, ...)` |
| Apex constructed records, no SOQL involved | `stripInaccessible` |
| Backwards compat to API 47 or earlier | `WITH SECURITY_ENFORCED` (legacy) |
| Need granular per-field reporting (which fields were removed) | `stripInaccessible` (USER_MODE throws, doesn't strip) |

Rule of thumb: `WITH USER_MODE` is THROW-on-inaccessible, `stripInaccessible` is REMOVE-and-continue. Pick based on whether silent partial success is acceptable.

### Performance — per-record FLS evaluation

`stripInaccessible` evaluates FLS for every field on every record. For large lists (10k+) it consumes CPU. It does NOT count against SOQL/DML governor limits, but it does count against the 10-second sync CPU limit. Profile before strip-then-DML on huge collections; consider chunking.

### Gotcha: parent-child relationships are NOT recursively stripped

`stripInaccessible` evaluates fields on the SObject collection you pass. If a `Case` carries a populated `Contact` lookup with nested fields (e.g., `caseRec.Contact.Email`), those nested fields are NOT evaluated. You must strip child collections separately.

```
SObjectAccessDecision parents  = Security.stripInaccessible(AccessType.UPDATABLE, cases);
SObjectAccessDecision contacts = Security.stripInaccessible(
    AccessType.UPDATABLE,
    Pluck.contacts(parents.getRecords())  // your own helper
);
```

### Interaction with Schema.SObjectField.isCreateable / isUpdateable / isAccessible

`Schema.DescribeFieldResult.isCreateable()` is a CHEAP proactive check — use it to fail fast before assembling a payload. `stripInaccessible` is an EXPENSIVE post-hoc scrub — use it as the enforcement gate. They are complementary, not redundant.

```
if (!Schema.sObjectType.Account.fields.AnnualRevenue.isCreateable()) {
    throw new AuraHandledException('You cannot set AnnualRevenue.');
}
// later, still call stripInaccessible before insert as defense-in-depth.
```

### Testing with System.runAs

Tests run as the system user by default — FLS is bypassed. To prove stripInaccessible actually strips, create a non-admin user with limited permissions and wrap your test in `System.runAs(testUser)`. Without `runAs`, your strip call returns the input unchanged and your test passes for the wrong reason.

---

## Common Patterns

### Pattern: REST endpoint — strip before DML

**When to use:** `@RestResource` accepting a JSON body of records.

**How it works:** Deserialize, strip with the appropriate AccessType, log removed fields for audit, DML on the stripped result.

**Why not the alternative:** Trusting the JSON payload assumes the running user can set every field they sent — they often cannot.

### Pattern: Aura/LWC controller — strip on the way out

**When to use:** `@AuraEnabled` method returning records to the client.

**How it works:** Query with `WITH USER_MODE` (preferred) OR strip the result with `AccessType.READABLE` before returning. Returning unscrubbed records lets the client see fields the user cannot read in the UI.

### Pattern: Defense-in-depth with WITH USER_MODE + stripInaccessible

**When to use:** The query already uses USER_MODE, but you also need to write back updates from a partially user-controlled object.

**How it works:** USER_MODE protects the read; stripInaccessible(UPDATABLE) protects the write. Each guards a different operation — do not collapse them.

---

## Decision Guidance

| Situation | Recommended Approach | Reason |
|---|---|---|
| REST/LWC inserts user-supplied records | `stripInaccessible(CREATABLE, ...)` then DML on `getRecords()` | Records originate from a less-trusted client |
| Aura controller returns records | `WITH USER_MODE` on the SOQL | Throws on inaccessible read, no silent strip |
| Upsert from a user-supplied list | `stripInaccessible(UPSERTABLE, ...)` | Intersects creatable AND updatable |
| Internal trigger handler updating fields the running user can edit anyway | Skip — not user-supplied | stripInaccessible cost not justified |
| Need to know WHICH fields got removed for audit | `stripInaccessible` + `getRemovedFields()` | USER_MODE throws, gives no per-field detail |
| Running on API < 48 | `WITH SECURITY_ENFORCED` | stripInaccessible introduced in API 45, USER_MODE in 58 |

---

## Recommended Workflow

1. Identify entry points where records cross from less-privileged caller (LWC, Aura, REST, Visualforce) into Apex. Mark every parameter.
2. For each entry point, pick the correct `AccessType` based on the DML you'll perform (CREATABLE for insert, UPDATABLE for update, UPSERTABLE for upsert, READABLE for outbound payloads).
3. Replace the raw DML on the input list with DML on `Security.stripInaccessible(type, input).getRecords()`. Capture the decision in a local variable so you can also call `getRemovedFields()`.
4. Strip child collections separately if relationships are populated — parent strip does NOT recurse.
5. Log `getRemovedFields()` (or surface as a warning to the caller) so silent stripping is observable.
6. Add a test under `System.runAs(nonAdminUser)` that proves a restricted field IS stripped. The same test under default system context should be a no-op.
7. Run `python3 scripts/check_apex_stripinaccessible_and_fls_enforcement.py` over the changed `.cls` files to catch the "stripped-then-DML-on-original" mistake.

---

## Review Checklist

- [ ] Every user-supplied record list passes through `Security.stripInaccessible` before DML
- [ ] DML targets `decision.getRecords()`, never the original argument
- [ ] `AccessType` matches the DML operation (CREATABLE/UPDATABLE/UPSERTABLE)
- [ ] Outbound query results either use `WITH USER_MODE` or `AccessType.READABLE` strip
- [ ] Child collections are stripped independently when relationships are populated
- [ ] `getRemovedFields()` is logged or surfaced; silent stripping is forbidden
- [ ] Test class uses `System.runAs(nonAdminUser)` and asserts a restricted field was removed
- [ ] No double-enforcement (USER_MODE in SOQL AND READABLE strip on the same path)

---

## Salesforce-Specific Gotchas

1. **DML on the original list silently bypasses the strip.** `stripInaccessible(...).getRecords()` returns a NEW list. Calling `insert userSupplied;` after the strip line does nothing — the strip's effect is in `getRecords()`.
2. **Parent strip does not recurse into child collections.** `Security.stripInaccessible(UPDATABLE, cases)` does not evaluate fields on `case.Contact.*`. Strip child collections separately.
3. **Tests pass under system context regardless of FLS.** Without `System.runAs(nonAdminUser)`, every field is accessible and your strip call is a no-op — the test proves nothing.
4. **`AccessType.UPSERTABLE` is the intersection of CREATABLE and UPDATABLE.** A field accessible for create but not update gets stripped on UPSERTABLE — this is correct behavior for upsert but surprising if you expected union semantics.
5. **`SObjectAccessDecision` is immutable.** You cannot mutate `getRecords()` results and have those changes reflect anywhere; call DML directly on the returned list.

---

## Output Artifacts

| Artifact | Description |
|---|---|
| Sanitized DML payload | `decision.getRecords()` — the only safe input to `insert`/`update`/`upsert` |
| Removed-fields audit | `decision.getRemovedFields()` map for logging or warnings |
| FLS enforcement test | Test under `System.runAs(nonAdmin)` proving restricted field stripped |
| AccessType decision note | Documented choice of CREATABLE / UPDATABLE / UPSERTABLE / READABLE per entry point |

---

## Related Skills

- `apex/apex-with-user-mode-soql` — when to prefer USER_MODE on the SOQL itself
- `apex/apex-sharing-keywords` — class-level `with sharing` / `without sharing` / `inherited sharing` (record visibility, complements FLS)
- `apex/apex-security-utils` — using the shared `templates/apex/SecurityUtils.cls` helpers
- `security/security-fls-crud-enforcement` — broader CRUD/FLS strategy across the org

Related Skills

mfa-enforcement-strategy

8
from PranavNagrecha/AwesomeSalesforceSkills

Plan and operate Salesforce org-wide multi-factor authentication (MFA) enforcement: verification methods, phased rollout, SSO and API-only considerations, exemptions, and operational readiness. NOT for designing Login Flow post-authentication logic, IP allowlists, or conditional step-up policies—use ip-range-and-login-flow-strategy, network-security-and-trusted-ips, or transaction-security-policies instead.

mfa-enforcement-patterns

8
from PranavNagrecha/AwesomeSalesforceSkills

Design MFA enforcement: auto-enablement, Salesforce Authenticator rollout, exceptions, service accounts, API-only users, SSO interop, and audit. Trigger keywords: MFA, multi-factor, two-factor, Salesforce Authenticator, MFA exception, MFA SSO, api-only MFA. Does NOT cover: end-user password policies, device-trust posture, or non-Salesforce IdP configuration.

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.