SfSkills Domains Docs GitHub

Home / data / crm-analytics-security-predicates

crm-analytics-security-predicates

Row-level security in CRM Analytics datasets via security predicates — SAQL filter expressions stored on the dataset that apply at query time per running user.

Open the skill folder on GitHub


CRM Analytics Security Predicates

Row-level security in CRM Analytics. A security predicate is a SAQL filter expression stored at the dataset level. Every query that reads the dataset has the predicate applied automatically, scoped to the running user's context (Id, role, profile, custom User fields). The dataset's owner / admin doesn't see it; the running user does.

What this skill is NOT. Salesforce Core SObject sharing rules (Org-Wide Defaults, role hierarchy, sharing rules, manual sharing, Apex managed sharing) operate on the source records — predicates operate on the dataset's copy of those records. Both can apply; they're independent. App / Dashboard / Lens sharing is different — that's CRM Analytics Asset sharing (who can open the dashboard at all), not row-level filtering inside it. Field-level masking is a different control (use Shield Platform Encryption or transform the dataset to omit / hash the field).


Before Starting


Core Concepts

Predicate syntax

'<DatasetColumn>' <operator> <value> [<logical-op> '<DatasetColumn>' <operator> <value>] ...

Components:

$User.* context variables

Available inside any predicate value position. They resolve at query time to the running user:

VariableResolves to
"$User.Id"The 18-character Id of the running user
"$User.UserRoleId"The role Id (or null if no role)
"$User.ProfileId"The profile Id
"$User.UserName"The Username
"$User.Email"Email
"$User.<CustomField__c>"Any standard or custom User field

The custom-field form is the most useful — define a Region__c on User, populate per user, then write 'Region' == "$User.Region__c" to filter the dataset by the running user's region.

The five access patterns and their predicate shapes

Access patternPredicate
Owner-only'OwnerId' == "$User.Id"
Owner OR my team'OwnerId' == "$User.Id" || 'TeamMembers' matches "$User.Id"
Role hierarchy (my role and below)'UserRoleId' in [matches "$User.UserRoleId"] (predicate refers to a precomputed role-hierarchy column in the dataset)
Region-scoped (custom User field)'Region' == "$User.Region__c"
Multi-condition: region AND own team('Region' == "$User.Region__c") && ('TeamMembers' matches "$User.Id")

Admins bypass predicates by default

CRM Analytics admins (users with the Manage Analytics permission) see all rows regardless of the predicate. This is intentional — it prevents admins from being locked out of their own datasets — but it's the source of the most common testing failure: the team writes the predicate, the admin tests it, sees all the rows, declares it working, and discovers in production that non-admin users see what they expected.

Always test with a non-admin user.

Predicate cost

Predicates run on every query against the dataset. The cost is:

For most predicates, the cost is negligible (sub-100ms added). For predicates with regex matches against role-hierarchy columns over millions of rows, the cost can be a noticeable fraction of dashboard load time. Profile in CRM Analytics's query inspector if a predicate-protected dashboard is slow.


Common Patterns

Pattern A — Owner-only access

When to use. Every user sees only the rows they own. Standard "my pipeline" / "my cases" semantics.

'OwnerId' == "$User.Id"

Test plan.

Trade. Doesn't surface team-shared visibility. If the user has been granted access via a sharing rule, they still don't see the row in the dataset because the predicate is per-dataset, not per-source-row. For team visibility, use Pattern B.

Pattern B — Owner OR team-member

When to use. Standard sharing-rule equivalent for opportunities or accounts where team members get visibility.

'OwnerId' == "$User.Id" || 'AccountTeamMembers' matches "$User.Id"

Prerequisite. The dataset must include a column listing team-member Ids per row — typically a multi-value column populated by the dataflow / recipe joining AccountTeamMember against Account.

Test plan.

Pattern C — Role hierarchy (my role and below)

When to use. Sales managers see rows owned by their reports.

'UserRoleHierarchyIds' matches "$User.UserRoleId"

Prerequisite. UserRoleHierarchyIds is a dataset column that contains, for each row, the chain of role Ids from the row owner up to the org top — populated by the dataflow / recipe.

Implementation note. Computing the role chain per row is a dataflow / recipe responsibility. The predicate is straightforward; the data preparation is where the cost lives.

Pattern D — Multi-dimensional (region AND team)

When to use. A regional manager sees rows in their region AND on their team — but not other regions, even if they're on the team.

('Region' == "$User.Region__c") && ('AccountTeamMembers' matches "$User.Id")

Trade. Adding dimensions multiplies the test matrix. Each new condition adds a new dimension of test users.

Pattern E — Service-account bypass for system queries

When to use. A scheduled SAQL query runs as a service user that must see all rows (export to a warehouse, daily aggregate). The predicate would block it.

Approach. Grant the service user the Manage Analytics permission so the predicate is bypassed. Document the bypass: a service-account-only role + permission set, audit-logged, scope limited to the relevant dataset(s).

Anti-pattern. Writing ('OwnerId' == "$User.Id" || 'OwnerId' == 'service-user-id') — works but ties the predicate to a specific user record. If the service user is recreated, the predicate silently breaks.


Decision Guidance

SituationPredicateReason
Standard "my pipeline" semantics'OwnerId' == "$User.Id" (Pattern A)Cleanest; no extra dataset prep
Sharing-rule equivalent (owner or team)Pattern B with team-member columnMirrors Salesforce sharing semantics in the dataset
Manager sees their reportsPattern C with role-hierarchy columnStandard hierarchical access
Region-scoped reporting'Region' == "$User.Region__c" (custom User field)Use custom User fields for non-standard dimensions
Multi-dimensional accessPattern D — combine with &&Each new dimension multiplies test users
Need different access per dashboardEither separate datasets or predicate that varies via $User.*Predicates are per-dataset, not per-dashboard
Service account that needs full accessManage Analytics permission on the service userDon't hardcode user Ids in the predicate
Test passes for admin onlyTest with a non-admin userAdmins bypass predicates by default
Predicate is slowing dashboardsProfile in CRM Analytics query inspector; consider precomputing fields in the datasetPer-row evaluation cost adds up

  1. Decide access pattern. Pick from owner-only, owner+team, role-hierarchy, region, multi-dimensional. Start simple; add dimensions only when justified.
  2. Identify the columns the predicate needs. Owner Id is usually present; team-members and role-hierarchy chains are dataflow / recipe-computed columns.
  3. Write the predicate following the syntax in Core Concepts.
  4. Define the test matrix. Owner / non-owner / admin / cross-team — one user per axis. Include a non-admin user; admins bypass predicates.
  5. Apply via the dataset's Security Predicate field (Setup or via the dataset XMD JSON).
  6. Test. Run the same SAQL query as each test user; confirm the row sets match expectations.
  7. Profile. If the dataset feeds latency-sensitive dashboards, run a representative query in the inspector and confirm predicate cost is acceptable.

Review Checklist


Salesforce-Specific Gotchas

  1. Admins bypass predicates by default. Test with a non-admin user, always. (See references/gotchas.md § 1.)
  2. Predicates apply to the dataset, not the dashboard. A predicate set on dataset X filters every dashboard that reads dataset X. (See references/gotchas.md § 2.)
  3. Predicates do NOT replace SObject-level sharing on source records. Both layers apply. The dataflow / recipe ran as a privileged user, so the dataset has the data; the predicate is the only filter the running user sees through. (See references/gotchas.md § 3.)
  4. $User.<CustomField__c> resolves at query time — if the custom field is null on the running user, the predicate's right-hand side is null, and 'Column' == null returns no rows for that user. (See references/gotchas.md § 4.)
  5. Role-hierarchy access requires a precomputed column in the dataset. SAQL can't traverse the role hierarchy at query time; the dataflow / recipe must produce a UserRoleHierarchyIds chain per row. (See references/gotchas.md § 5.)
  6. matches regex is more expensive than == and in. Profile if the dashboard is slow. (See references/gotchas.md § 6.)
  7. Hard-coding a service-account user Id in the predicate breaks when the user is recreated. Use the Manage Analytics permission to bypass instead. (See references/gotchas.md § 7.)

Output Artifacts

ArtifactDescription
Security predicate expressionThe actual string applied to the dataset's Security Predicate field
Dataflow / recipe additionsAny precomputed columns the predicate depends on (team membership, role-hierarchy chain)
Custom User fields usedPer-user values referenced via $User.<CustomField>
Test matrixOwner / non-owner / admin / cross-team users with expected row sets per user
Performance baselineQuery inspector output before/after predicate; documented overhead

Package files