Home / apex / field-level-security-in-async-contexts
field-level-security-in-async-contexts
Use when async Apex (Queueable, Batch, Schedulable, @future) needs to honor the originating user's field-level security but the job runs in a different security context than the user who initiated it.
Open the skill folder on GitHub
Field-Level Security in Async Contexts
Activate when Apex code that started in one user's transaction is continued asynchronously (Queueable, Batch, Schedulable, @future) and the FLS rules that applied in the original transaction must still apply in the async job. The skill produces a pattern for capturing and re-applying the originating user's FLS, plus tests that catch silent bypass.
Before Starting
Gather this context before working on anything in this domain:
- Who enqueued the work, and who runs it? A trigger fired by user A enqueues a Queueable. The Queueable's
execute()runs as user A only because the trigger's transaction continues; but a Batch job kicked off by user A fromDatabase.executeBatch()runs in each batch slice as user A as well — yet a Scheduled Apex job runs as the user who scheduled it, which may be a sysadmin who set up the schedule years ago. Get this straight before reasoning about FLS. - What FLS does the async job need to respect? Sometimes the requirement is "honor the user who pressed the button" (most UIs); sometimes it's "honor a service identity" (integration jobs); sometimes it's "system context, do everything" (cleanup jobs). The wrong choice silently leaks or blocks fields.
- Where does the data leave the system? FLS enforcement matters most at the boundary: writing to a record (DML), returning to a UI, or sending to an external callout. An async job that only logs IDs internally has lower FLS surface than one that emails a CSV export.
Core Concepts
Async transitions change the user-mode evaluation point, not the user
The user that an async job runs as is determined at enqueue time, not at execute time. Specifically:
| Async surface | Running user |
|---|---|
Queueable / @future | The user who enqueued the job — same as the original synchronous transaction |
Database.executeBatch() | The user who called executeBatch(). If that was an Apex Scheduler firing, the user is whoever scheduled the scheduler |
System.schedule() / scheduled Apex | The user who scheduled the job (often a long-departed sysadmin) — most common source of FLS surprises |
Platform-Event-triggered Apex (after insert on __e) | The Automated Process user, which bypasses FLS and CRUD entirely — documented but routinely missed |
WITH USER_MODE, Security.stripInaccessible(), and the default user mode that database operations get at apiVersion 67.0+ all evaluate against UserInfo.getUserId() at the moment the call executes. They don't know which user originally requested the work. If the requesting user and the running user differ, the security check answers a different question than the practitioner expected. (The older WITH SECURITY_ENFORCED had the same running-user blind spot, but it was removed in API 67.0 and does not compile on a class pinned at 67.0 or above — write WITH USER_MODE. Per-version detail: