all insights

Querying Data 360 with SOQL: the SET OPTIONS clause, dataspaces, and the limits that bite

For years, reading unified data back out of Data Cloud from Apex meant the Query API or ConnectApi and a lot of typecasting. Native SOQL changed that — and Summer '26 added a SET OPTIONS clause that finally lets SOQL reach Data Lake Objects with dataspace scoping. Here's the exact syntax, the NULL-versus-empty-string trap, the callout-and-credit semantics that surprise people, and the limits that send you back to the Query API.

Querying Data 360 with SOQL: the SET OPTIONS clause, dataspaces, and the limits that bite — article illustration

You spent two quarters getting data into Data 360 — ingesting streams, resolving identities, building a clean unified profile. Now the other direction: a Lightning component, a Flow, an Apex trigger, all sitting right there in the same org, need to read those unified records. And the way a Salesforce developer’s hands want to do that is the way they do everything else — a SOQL query. For a long time, that instinct hit a wall. Data Cloud objects weren’t standard sObjects; you reached them through the Query API over REST, or through ConnectApi/CdpQuery in Apex with a layer of manual typecasting on top. Native SOQL against your unified data simply wasn’t a thing.

That has quietly become one of the more useful capabilities on the platform, and the Summer ‘26 release rounded off the sharp edge that was left. This post is the working developer’s guide to native SOQL against Data 360: what you can query and what you can’t, the new SET OPTIONS clause that scopes a query to a dataspace and fixes the NULL-versus-empty-string mismatch, the Apex semantics that count a query as a callout and bill you credits for it, and the concrete limits that will, at some point, send you back to the Query API. Every syntax example here is lifted from the official v67.0 reference.

One naming note first, because it’s in every API name and error message: Salesforce rebranded Data Cloud to Data 360 on October 14, 2025. The platform, the objects, the ssot__ prefixes, and the SOQL surface are the same; older docs and endpoints still say “Data Cloud.” Read the two as synonyms, and see the rebrand explainer if you want the history.

A short history, because it explains the limits

The constraints make sense once you know the sequence.

  • Before API 61.0, the platform path to Data Cloud was the Query API and the ConnectApi/CdpQuery Apex classes. No native SOQL, and results came back needing typecasting.
  • API 61.0 (Summer ‘24) made static SOQL against Data Model Objects (DMOs) work end to end — strongly-typed DMO sObjects (no more CdpQuery casting), Database.QueryLocator and for loop support, and SObjectType.getDescribe() on DMOs. Before 61.0, a DMO query returned only the first 201 records, which is exactly the kind of silent cap that produces a bug report three sprints later.
  • Summer ‘26 (API v67.0) added the SET OPTIONS clause, which does two things: it lets SOQL reach Data Lake Objects (DLOs) by scoping the query to a dataspace, and it exposes a honorEmptyStrings switch to control the NULL/empty-string behavior that had been quietly biting DLO queries.

So native SOQL against Data 360 is not one feature — it’s a DMO capability from 2024 that grew DLO reach in 2026. Keep that split in mind; it maps directly onto what SET OPTIONS is and isn’t for.

What you can actually query — and the naming that trips people

Three object families are SOQL-reachable, and one common one isn’t:

ObjectSuffixExampleNotes
Data Model Object (DMO)__dlmssot__Individual__dlmStandard DMOs carry the ssot__ prefix; standard fields are ssot__…__c. Fully SOQL-queryable.
Unified profile DMO__dlmUnifiedIndividual__dlmThe golden records from identity resolution. Queried like any DMO.
Data Lake Object (DLO)__dllWebEngagement__dllQueryable via SOQL only with a dataspace in SET OPTIONS (below).
Calculated Insight Object (CIO)Not SOQL-queryable. Reach these through the Query API instead.

The DLO-versus-DMO distinction isn’t cosmetic here — it decides whether you need SET OPTIONS at all. A DMO query works like a normal SOQL query. A DLO query does not, until you tell it which dataspace to look in. And because API names vary with how objects were created, confirm the exact name of your object in Setup rather than assuming a suffix — the reference’s own examples are inconsistent about it.

The SET OPTIONS clause

Here’s the new grammar, verbatim from the v67.0 SOQL reference. The clause goes at the very end of the query — after ORDER BY, LIMIT, OFFSET, and the FOR/UPDATE clauses, right before FOR UPDATE:

[SET OPTIONS ( optionName = optionValue [, optionName = optionValue])]

Two options exist today, and their scopes differ in a way worth memorizing.

dataspace — required for DLOs, and the fix for the “zero records” mystery

If you’ve ever run a perfectly valid SOQL query against a DLO and gotten zero rows back with no error, this is why. The reference is blunt about it: “If the dataspace isn’t specified, the query returns zero records.” The dataspace option names which dataspace to query, and it is valid only for DLO queries — not supported for DMO queries.

SELECT Id, VisitorId__c, PageUrl__c
FROM WebEngagement__dll
WHERE SessionActive__c = true
SET OPTIONS (dataspace = 'default')

Historically, DLOs assigned to a custom (non-default) dataspace couldn’t be queried through SOQL in tools like Developer Console or Workbench at all — those tools assumed the default dataspace, and the workaround was to map the DLO to a custom DMO and query that. The dataspace option is the direct fix for that whole class of pain: name the dataspace and the DLO is reachable.

honorEmptyStrings — the NULL-versus-empty-string trap

This one causes wrong results rather than no results, which is worse. Salesforce Platform objects treat NULL and an empty string '' as the same value — a filter on either returns the same records. Data 360 DLOs treat them as different values. So a query that behaves one way against CRM data behaves differently against a DLO, and nobody told you.

honorEmptyStrings controls it, and it works for both DLOs and DMOs:

  • honorEmptyStrings = false — the default — collapses NULL and '' into one value, consistent with Platform objects.
  • honorEmptyStrings = true — treats NULL and '' as distinct, so filtering returns different record sets depending on which one the field actually holds.
-- Returns rows where EmailOptIn__c is an empty string ONLY,
-- not rows where it is NULL:
SELECT Id, EmailOptIn__c
FROM ContactPoint__dll
WHERE EmailOptIn__c = ''
SET OPTIONS (dataspace = 'default', honorEmptyStrings = true)

The practical rule: if your ingested data distinguishes “we know it’s blank” from “we never got a value” — and a lot of ingested data does — set honorEmptyStrings = true and be explicit in your filters, because the default will merge two states you probably care about keeping apart.

A status note, since this is new: SET OPTIONS is documented as standard SOQL in the v67.0 reference with no preview marker, but it arrived in Summer ‘26 and at least one practitioner writeup has described it as still stabilizing. Before you build a critical path on it, confirm the current status and supported operators against the release notes for your org’s API version.

Apex: it looks like SOQL, it behaves like a callout

This is the section that saves you a production incident. Inside Apex, a static SOQL query against a DMO looks exactly like any other query:

// Static, strongly-typed SOQL against a unified-profile DMO
List<UnifiedIndividual__dlm> profiles = [
    SELECT Id,
           ssot__FirstName__c,
           ssot__LastName__c,
           ssot__Email__c
    FROM UnifiedIndividual__dlm
    WHERE ssot__CompanyId__c = :companyId
];

Clean. But three behaviors underneath it are not like normal SOQL, and each has bitten someone:

1. It’s a callout. The Apex guide states it plainly: “A static SOQL query against Data 360 from Apex is considered a callout and is subject to the same restrictions as HTTP callouts from Apex.” The immediate consequence is the pending-DML rule — run a Data 360 query after uncommitted DML in the same transaction and you get:

UnexpectedException: A callout was unsuccessful because of pending uncommitted work
related to a process, flow, or Apex operation. Commit or roll back the work, and then try again.

So the Data 360 read has to happen before your DML, or in a separate transaction. Treat it like an external call, because that’s what it is.

2. It costs credits. Every DMO query consumes Data Services credits from your Data 360 subscription. The guide’s own warning calls out “FOR loops, query locators, recursion, or any mechanism that can result in multiple queries to Data 360” — the exact patterns that turn one logical read into fifty billed ones. Querying Data 360 in a loop is the platform-SOQL equivalent of a chatty API integration, and it lands on the same credit meter. Bulkify the read: pull what you need in one query keyed on a set of IDs, not one query per record.

3. The security model is coarser than you expect. DMOs are accessible from Apex in system mode, and — this is the part to write down — there is currently no field-level security and no record-level access control for them. WITH USER_MODE, WITH SECURITY_ENFORCED, and Security.stripInaccessible() can check only object-level access on a DMO, gated by whether the running user has access to the dataspace. If your design assumed FLS would quietly filter sensitive columns out of a Data 360 query, it won’t. You enforce that yourself. One more gotcha: Schema.getGlobalDescribe() can’t discover DMOs — you describe them explicitly with Schema.describeSObjects(List<String>) using known API names.

For volume, Database.QueryLocator and for-loop iteration over DMOs are supported from API 61.0 onward (below that, the 201-record cap). Batch Apex is blocked against DMOs when using a QueryLocator, but works with an Iterable.

The limits that send you back to the Query API

Native SOQL is a genuine convenience, but it is a subset. The moment your access pattern outgrows it, you’ll feel a hard edge — and knowing them in advance is the difference between choosing the right tool and discovering the wrong one at 5pm.

  • No JOINs. SOQL in Data 360 does not support relationship traversal the way you’re used to — “parent and child relationships… aren’t supported in the current implementation of SOQL in Data 360,” and child-to-parent relationships specifically aren’t supported. Semi-joins exist but are narrow: they must use lookup fields, and a DMO-to-CRM semi-join caps the inner CRM query at 2,000 records and can’t trigger queryMore() on the outer DMO query. Anything genuinely relational across multiple objects is a Query API job.
  • No SELECT *. Use FIELDS(ALL) or name the fields explicitly. (Note this is a SOQL rule; Data 360 SQL — a different dialect — does support *. Don’t mix them up.)
  • A 12-MB result cap. “Data 360 limits SOQL results to 12-MB,” returning up to the limit with a “query more” link to paginate. But if a single result row set exceeds 12 MB, the query is rejected outright and you get nothing — so wide SELECTs over big DLOs are a trap.
  • Aggregate and currency quirks. AVG, SUM, COUNT, MIN, MAX are supported on DMOs and DLOs, but “queries that contain aggregate functions don’t support currency fields even when the fields are outside the aggregate functions,” you can’t use Id in GROUP BY or HAVING, and HAVING doesn’t support IN or comparisons to NULL.
  • String comparison operators are limited. Only the default Unicode Root Collation is supported, and the range operators >, <, >=, <= are not supported on string fields. A WHERE Text_Field > 'M' won’t do what you want.

None of these is a defect; they’re the boundary of a lightweight, on-platform query path. When you hit them, the Data 360 Query API is the other door — full ANSI SQL with real joins, window functions, and large extracts, reachable from any external system. The decision rule is clean: SOQL when you’re a Salesforce dev already on-platform doing single-object or simple-filter reads and you want typed sObjects; the Query API when you need joins, heavy aggregation, or volume. The older ConnectApi path still exists for object-specific profile and insight retrieval where a dedicated endpoint fits better.

Testing Data 360 SOQL without melting credits

Because these queries are callouts that bill credits, you don’t want your unit tests hammering the real service. Salesforce gives you two answers, at two maturity levels.

Mock SOQL stubs (generally available). You stub the query with a provider that extends System.SoqlStubProvider, register it with Test.createSoqlStub(), and build synthetic rows with Test.createStubQueryRow() — no real callout, no credits, deterministic tests:

SoqlStubProvider stub = new UnifiedIndividualSoqlStub();
Test.createSoqlStub(UnifiedIndividual__dlm.sObjectType, stub);
Assert.isTrue(Test.isSoqlStubDefined(UnifiedIndividual__dlm.sObjectType));

The stub target must be a DMO or external object, and inside the stub you can’t run SOQL, SOSL, callouts, future methods, queueables, batch jobs, DML, or platform events — it’s a pure data provider.

Apex Integration Tests (Developer Preview, Summer ‘26). For when a mock isn’t enough and you want to validate against the real service, Summer ‘26 introduced integration tests that relax callout and rollback restrictions so a test can make real calls and commit mid-transaction — including “real SOQL queries against Data 360 data model objects (DMOs) without stub mocks.” The important caveat, because it’s a developer preview: it’s scratch-org only (enabled with "features": ["ApexIntegrationTests"] in your scratch definition), and you can’t run it in production or during metadata deployments. Use it to validate behavior in a scratch org; keep mocks for your deployable test suite.

The takeaway

Native SOQL against Data 360 turned a REST-and-typecasting chore into something a Salesforce developer can write from muscle memory — and Summer ‘26’s SET OPTIONS clause closed the two gaps that made DLO queries frustrating: it scopes a query to a dataspace (without which a DLO query silently returns nothing) and it lets you honor the DLO distinction between NULL and an empty string that Platform objects collapse. Reach for it when you’re on-platform doing straightforward reads of DMOs and unified profiles, remember that in Apex it’s a credit-billed callout with a coarse security model and no FLS, bulkify to keep the meter down, and the moment you need a real join or a big extract, walk over to the Query API. Get that division of labor right and Data 360 stops being a place data goes to hide and becomes just another thing your org can query.

Understanding the basics

Can you query Data 360 (Data Cloud) objects with SOQL?

Yes. Since API 61.0 (Summer ‘24) you can run static, strongly-typed SOQL against Data Model Objects (DMOs), including unified-profile objects like UnifiedIndividual__dlm, from Apex, Flow, and LWC. Summer ‘26 (API v67.0) extended reach to Data Lake Objects (DLOs) through the new SET OPTIONS clause, which scopes the query to a dataspace. Calculated Insight Objects (CIOs) remain non-queryable via SOQL — use the Query API for those. SOQL is the on-platform path for single-object and simple-filter reads; the Query API is for joins, heavy aggregation, and large extracts.

What does the SET OPTIONS clause do in SOQL?

SET OPTIONS goes at the very end of a SOQL query and configures behavior when querying Data 360. It supports two options. dataspace = '<name>' specifies which dataspace to query and is required for DLO queries — omit it and the query returns zero records; it isn’t supported for DMO queries. honorEmptyStrings (default false) controls NULL versus empty-string handling: false treats NULL and '' as the same value, like Salesforce Platform objects; true treats them as distinct, which matters because Data 360 DLOs store them differently. honorEmptyStrings works for both DLOs and DMOs.

Why does my Data 360 SOQL query return zero records?

The most common cause on a Data Lake Object is a missing dataspace. Data 360 requires a dataspace in the SET OPTIONS clause for DLO queries, and if it isn’t specified the query returns zero records with no error. Add SET OPTIONS (dataspace = 'default') (or your custom dataspace name) to the end of the query. If you’re filtering on blank values, also check honorEmptyStrings: with the default false, NULL and empty strings are merged, which can make a filter match — or miss — records you didn’t expect.

Does querying Data 360 from Apex use credits?

Yes. A static SOQL query against a DMO from Apex is treated as a callout and consumes Data Services credits from your Data 360 subscription. Salesforce specifically warns about for loops, query locators, and recursion, because they can fire many queries and multiply the credit cost. Bulkify: query once against a set of IDs rather than once per record, run the Data 360 read before any DML in the transaction (a query after uncommitted DML throws a pending-work exception), and use mock SOQL stubs in your unit tests so test runs don’t hit the real service or burn credits.


Wiring Data 360 into on-platform Apex, Flows, and components — and trying to decide where SOQL is enough and where you need the Query API, without running up a surprise credit bill? Talk to us. Getting the query layer and the cost model right is exactly the kind of data-foundation work we do.

Keep reading

All insights