SfSkills Domains Docs GitHub

Home / apex / soql-for-view-and-for-reference

soql-for-view-and-for-reference

Use when a SOQL query that serves a custom record-viewing surface (custom LWC/Aura/Visualforce page, mobile app, or API client) must make the retrieved records show up in the user's Recent Items and global-search auto-complete by writing LastViewedDate / LastReferencedDate and a RecentlyViewed row via the optional FOR VIEW / FOR REFERENCE clauses.

Open the skill folder on GitHub


SOQL FOR VIEW and FOR REFERENCE

This skill activates when a custom record-viewing surface — a Lightning web component, Aura component, Visualforce page, mobile app, or API client — must make the records it fetches behave like records the user "opened": appear in Recent Items and in the global-search auto-complete list. Standard Lightning record pages get this for free; a custom surface that runs its own SOQL does not, and must opt in with the optional FOR VIEW or FOR REFERENCE clause. These clauses turn a read into a read-plus-write: they update LastViewedDate / LastReferencedDate and add a RecentlyViewed row for each retrieved record. Misused, they silently pollute the user's recency data — Salesforce documents an explicit warning against that.

The official SOQL and SOSL Reference documents both clauses as current (Summer '26 / API 67.0) and does not stamp them Beta, Pilot, or deprecated. Do not assert a maturity level the docs do not state.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The two clauses and what each one writes

Both clauses are optional trailing SOQL clauses. Each performs two writes per retrieved record:

LastViewedDate drives the Recent Items list; LastReferencedDate reflects a lighter "the user saw a reference to this" signal. Both feed the global-search auto-complete / most-recently-used surface.

Where the clauses sit in SOQL grammar

The clauses are near the end of the SELECT statement, after ORDER BY, LIMIT, and OFFSET, and before the UPDATE {TRACKING | VIEWSTAT} and FOR UPDATE clauses:

[ORDER BY ...]
[LIMIT n]
[OFFSET n]
[{FOR VIEW | FOR REFERENCE}]
[UPDATE {TRACKING | VIEWSTAT}]
[FOR UPDATE]

Minimal working syntax (straight from the docs):

SELECT Name, Id FROM Contact LIMIT 1 FOR VIEW
SELECT Name, Id FROM Contact LIMIT 1 FOR REFERENCE

Updating both recency fields ("in conjunction with")

To refresh both recency fields, the docs say: "To update recent usage data for retrieved objects, use the FOR VIEW clause in conjunction with the FOR REFERENCE clause." Note the nuance: the reference shows the two clauses only individually and gives no combined single-query example, and the SELECT grammar presents them as an alternation {FOR VIEW | FOR REFERENCE}. In practice, pick the one clause that matches the surface (a real view → FOR VIEW; a reference → FOR REFERENCE); treat "update both fields from one query" as under-specified rather than asserting a combined syntax the reference does not print.

The RecentlyViewed lifecycle

RecentlyViewed is ephemeral, not an audit log:

So a record can silently drop out of Recent Items after 90 days, or once it falls outside the newest 200 rows for its object. Never build reporting or compliance logic on top of RecentlyViewed.


Common Patterns

Custom record viewer marks the record viewed (FOR VIEW)

When to use: a custom LWC/Aura/Visualforce page loads a single record for the user to read, replacing what a standard record page would have done, and you want that record to land in Recent Items.

How it works: in the selector layer, fetch the specific record by Id with a LIMIT and append FOR VIEW. The controller passes the opened record's Id; the query touches exactly that record.

SELECT Id, Name, StageName FROM Opportunity WHERE Id = :recordId LIMIT 1 FOR VIEW

Why not the alternative: without the clause the record never enters Recent Items from your custom surface (the platform only writes recency for standard pages). Writing LastViewedDate yourself via a Database.update is both unnecessary and, for RecentlyViewed, impossible — the clause is the supported mechanism.

Lightweight reference from a mobile app or custom page (FOR REFERENCE)

When to use: a mobile client or custom page surfaces a record as a reference — a card, a preview, a lookup result — without a full page view, and you want a "recently referenced" signal.

How it works: same shape, FOR REFERENCE instead, writing LastReferencedDate:

SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1 FOR REFERENCE

Why not the alternative: using FOR VIEW here overstates the interaction (the user did not open the record), inflating the Recent Items list with things the user only glanced at a reference to.

Keep it bounded and user-scoped (misuse guard)

When to use: always, whenever either clause is present.

How it works: constrain the query to the specific record(s) the user is looking at (a WHERE Id = :id / WHERE Id IN :ids), add a LIMIT, and run it in user mode (WITH USER_MODE / AccessLevel.USER_MODE). This guarantees the clause only writes usage data for records the user can actually see and is actually viewing.

Why not the alternative: an unbounded SELECT ... FOR VIEW (no WHERE, no LIMIT) stamps LastViewedDate on up to the query's whole result set, flooding the user's Recent Items with records they never opened — exactly the misuse the docs warn against.


Decision Guidance

SituationRecommended ApproachReason
Custom UI shows a full view of a recordFOR VIEW on a bounded, by-Id queryWrites LastViewedDate; record enters Recent Items
Mobile app / custom page references a record without a full viewFOR REFERENCEWrites LastReferencedDate; "recently referenced" signal
You believe you need both recency fields updatedPick the clause matching the actual interaction; treat "both from one query" as under-documentedReference shows the clauses only individually, no combined example
Batch / trigger / @future / Queueable / scheduled / integration userNeither clauseNo logged-in user is viewing the records — the docs' misuse condition
Standard Lightning record pageNeither clauseThe platform already writes recency for standard views
Aggregate query (COUNT(), GROUP BY)Neither clauseThere are no individual records for the user to "view"
Custom object with no tabCreate a custom tab first (need not be visible), then use the clauseFields do not exist until a tab is defined

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Verify the context. Confirm a logged-in user is about to view (or reference) the retrieved records in this exact request. If the code path is batch, trigger, @future, Queueable, scheduled, or runs as an integration user, stop — do not add the clause.
  2. Pick the clause. FOR VIEW for a full record view; FOR REFERENCE for a lighter reference from a custom interface. If you think you need both recency fields, choose the one that matches the real interaction rather than inventing a combined form.
  3. Bound the query. Add a specific WHERE (the record Id / Ids the user opened) and a LIMIT, and keep the query in user mode so it can only touch records the user can see.
  4. Check field availability. Confirm the target object exposes LastViewedDate / LastReferencedDate. For a custom object, ensure a custom tab exists (visibility not required) or the query throws No such column.
  5. Place and locate the clause. Put it after ORDER BY / LIMIT / OFFSET and before FOR UPDATE, and keep the query in the selector layer (see templates/apex/BaseSelector.cls), not duplicated across controllers or inside loops.
  6. Run the checker and verify. Run scripts/check_soql_for_view_and_for_reference.py over the Apex source, then open the surface as a test user and confirm the viewed record appears in Recent Items / search auto-complete — and that unrelated records were not polluted.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. A "read" that writes — SELECT ... FOR VIEW performs DML side-effects (updates LastViewedDate and inserts RecentlyViewed rows). It looks like a query in code review but mutates data, which is why it must not run where no user is actually viewing the records.
  2. Misuse silently corrupts the user's experience — the docs warn to use the clauses "only when you are sure that the retrieved records will definitely be viewed by the logged-in user, else the clause incorrectly updates the usage information for the records." A single unbounded query can flood Recent Items and search auto-complete with noise.
  3. Custom objects fail until they have a tab — querying LastViewedDate / LastReferencedDate (or using these clauses) on a custom object without a custom tab throws No such column. Creating the tab — even one hidden from the navigation bar — enables the fields.
  4. RecentlyViewed is not permanent — data is retained 90 days then removed periodically, and is truncated to 200 records per object. Do not treat it as an audit trail or build compliance logic on it.

Output Artifacts

ArtifactDescription
FOR VIEW / FOR REFERENCE clauseAppended to a bounded, user-facing selector query at the correct grammar position
scripts/check_soql_for_view_and_for_reference.py reportFlags the clauses in bulk / async / system-context code, on unbounded or aggregate queries
templates/soql-for-view-and-for-reference-template.mdDecision worksheet + canonical safe selector snippet for a custom record viewer

Package files