Hands-on pipeline from raw data to AI agent: CRM data + Databricks Delta tables unified in Data Cloud (D360), grounded in Agentforce. Built to understand what D360 actually does at the infrastructure level, not just the slide deck version.
graph LR
subgraph Sources
CRM["Salesforce CRM<br/>25 Accounts, ~55 Contacts<br/>~35 Opps, 18 Cases"]
DBX["Databricks Delta Lake<br/>Web Analytics (~44 rows, user_email)<br/>Product Usage (~38 rows, user_email)<br/>Firmographic (25 rows, domain)"]
end
subgraph D360["Salesforce Data Cloud (D360)"]
direction TB
DLO["Data Lake Objects<br/>(raw, source schema)"]
DMO["Data Model Objects<br/>(normalized canonical)"]
IR["Identity Resolution<br/>(exact email match via<br/>Contact Point Email)"]
CI["Calculated Insights<br/>+ Segments"]
end
subgraph Output
AF["Agentforce Agent<br/>(grounded in unified data)"]
end
CRM -->|"CRM Native<br/>Ingestion (free)"| DLO
DBX -->|"Query Federation<br/>(Zero Copy, live)"| DLO
DLO --> DMO
DMO --> IR
IR --> CI
CI --> AF
D360's value proposition is straightforward: without it, your AI agent sees CRM data only. With it, the agent sees CRM + web engagement + product usage + third-party enrichment. The difference between "this account has an open deal" and "this account has an open deal, declining product usage, 3 escalated support tickets, and hasn't visited the pricing page in 60 days" is the difference between a chatbot and a useful agent.
- Evaluating D360 (Data Cloud) architecture for an implementation or proof of concept
- Have a Salesforce Developer Edition with Data Cloud enabled
- Have a Databricks workspace (trial works)
- Comfortable with Python and SQL
# 1. Clone and set up
git clone https://github.com/btriani/salesforce-d360-agentforce-lab.git
cd salesforce-d360-agentforce-lab
python3 -m venv .venv && source .venv/bin/activate
pip install -r d360-agentforce-lab/01-synthetic-data/requirements.txt
# 2. Authenticate to Salesforce (OAuth, no SOAP)
sf org login web --alias my-dev-org
# 3. Load CRM data
cd d360-agentforce-lab/01-synthetic-data
python generate_and_load.py
# 4. Create Databricks tables (set DATABRICKS_HOST and DATABRICKS_TOKEN)
cd ../02-external-data
python generate_external_data.py| # | Phase | What It Does | D360 Concepts |
|---|---|---|---|
| 01 | Synthetic CRM Data | Generate ~55 contacts, ~35 opps, 25 accounts, 18 cases and load into Salesforce via REST API | CRM-native ingestion, data model foundation |
| 02 | External Data | Create ~44 web analytics rows, ~38 product usage rows, 25 firmographic rows as Delta tables in Databricks | External sources, Zero Copy, Query Federation |
| 03 | D360 Configuration | Connect Databricks, create data streams, map DMOs, configure IR with email matching, build Calculated Insights and Segments | Data Streams, DLOs, DMOs, Identity Resolution, Calculated Insights, Segments |
| 04 | Agentforce Agent | Design agent with prompt templates grounded in unified D360 data | Prompt templates, Trust Layer, grounding |
The customer data platform space has several players. D360's differentiator isn't any single feature -- it's the tight integration between data unification, AI, and CRM action in one platform.
| Dimension | Salesforce D360 | Segment / Tealium | Databricks + Unity Catalog | Snowflake + dbt |
|---|---|---|---|---|
| CRM integration | Native -- zero-cost CRM ingestion, bidirectional | Connector-based, one-way | No CRM, requires ETL | No CRM, requires ETL |
| External data | Query Federation (Zero Copy), connectors, Ingestion API | SDKs, connectors | Native (it IS the lake) | Native (it IS the warehouse) |
| Identity resolution | Built-in, configurable match/reconciliation rules | Built-in, event-stream focused | Manual (write your own) | Manual (write your own) |
| AI/Agent integration | Agentforce natively grounded in unified data | None built-in | Mosaic AI, but no CRM context | Cortex, but no CRM context |
| Zero Copy | Yes -- Databricks, Snowflake, BigQuery, Redshift | No | N/A (data is already there) | N/A |
| Time to value | Fast for Salesforce shops, config-heavy for external data | Fast for web/mobile events | Requires engineering team | Requires engineering team |
| Vendor lock-in | High (Salesforce ecosystem) | Medium | Low (open formats) | Medium |
| Best for | Enterprises already on Salesforce wanting unified customer view + AI agents | Marketing teams needing event tracking + audience activation | Data teams wanting full control over the lakehouse | Analytics teams wanting governed warehouse |
Honest take: D360 shines when you're already in the Salesforce ecosystem and want AI agents that act on unified data. If your data already lives in Databricks/Snowflake and you don't use Salesforce CRM, D360 adds a layer you may not need. The Zero Copy federation is genuinely useful -- it avoids the "copy everything into our platform" trap that most CDPs fall into. The weakness is the configuration complexity: DMO mapping, identity resolution setup, and Contact Point plumbing require significant upfront effort.
graph TB
subgraph "Phase 1: CRM Data"
PY1["Python + simple_salesforce"] -->|"REST API"| SF["Salesforce CRM"]
SF -->|"Native Ingestion<br/>(free, automatic)"| DLO_CRM["DLOs: Account_Home<br/>Contact_Home<br/>Opportunity_Home<br/>Case_Home"]
end
subgraph "Phase 2: External Data"
PY2["Python + Databricks SQL API"] -->|"REST API"| DBX["Databricks<br/>Delta Tables"]
DBX -->|"Query Federation<br/>(Zero Copy, live queries)"| DLO_EXT["DLOs: Web Analytics<br/>(user_email keyed)<br/>Product Usage<br/>(user_email keyed)<br/>Firmographic<br/>(domain keyed)"]
end
subgraph "Phase 3: D360 Unification"
DLO_CRM --> DMO["Data Model Objects<br/>(normalized schema)"]
DLO_EXT --> DMO
DMO --> IR["Identity Resolution<br/>(exact email match)"]
IR --> UP["Unified Individuals"]
UP --> CI["Calculated Insights<br/>(Health Score)"]
CI --> SEG["Segments<br/>(At Risk / Healthy / Upsell Ready)"]
end
subgraph "Phase 4: AI Action"
SEG --> AGENT["Agentforce Agent"]
CI --> AGENT
UP --> AGENT
AGENT -->|"Next Best Action"| USER["Sales / CS Rep"]
end
graph LR
subgraph "CRM Contact (Salesforce)"
C["Contact Point Email:<br/>jane.doe@apexfintech.com"]
end
subgraph "Web Analytics (Databricks)"
W["user_email:<br/>jane.doe@apexfintech.com<br/>12 page views, 2 demo visits"]
end
subgraph "Product Usage (Databricks)"
P["user_email:<br/>jane.doe@apexfintech.com<br/>logins: 18, feature_adoption: 72%"]
end
C -->|"exact email match"| UP["Unified Individual:<br/>jane.doe@apexfintech.com<br/>(3 sources matched)"]
W -->|"exact email match"| UP
P -->|"exact email match"| UP
| Challenge | What We Did | Why It Matters |
|---|---|---|
| Partial coverage | Not all contacts appear in web analytics or product usage | Tests how D360 handles missing data across sources |
| Foreign keys | Product usage uses EXT-XXXXX IDs, not Salesforce IDs |
Proves identity resolution works without shared keys |
| Name variations | ~20% of firmographic names have "Inc.", "LLC", double spaces | Tests fuzzy matching in reconciliation rules |
| Individual-level keying | Web analytics and product usage keyed by user_email, not company domain |
Enables individual-level IR matching via Contact Point Email |
| Role-based exclusions | Generic emails (info@, support@) excluded from external data | Prevents false matches on shared mailbox addresses |
| Term | What It Actually Means |
|---|---|
| Data Stream | A pipeline that brings data into Data Cloud (like an ETL job) |
| DLO (Data Lake Object) | Raw data container -- preserves source schema as-is |
| DMO (Data Model Object) | Normalized canonical model -- the "T" in ELT |
| Query Federation | D360 queries external data live (Databricks, Snowflake) without copying it |
| Zero Copy | Marketing term for Query Federation + file sharing -- data doesn't move |
| Identity Resolution | Matching records across sources into unified profiles using match rules |
| Contact Point Email | Bridge object linking Individual DMO to email addresses -- the key to IR matching |
| Calculated Insight | A computed metric (e.g., Health Score) built on unified data |
| Segment | A group of entities filtered by criteria (e.g., "At Risk" accounts) |
| Agentforce | AI agent framework -- value is proportional to the data it's grounded in |
| Einstein Trust Layer | Governance layer ensuring agents only access permitted data |
| Profile vs Engagement | Profile = what something IS (attributes). Engagement = what something DOES (events) |
| Technology | Usage |
|---|---|
| Python 3.14 | Data generation (Faker), Salesforce REST API (simple_salesforce), Databricks SQL API |
| Salesforce Data Cloud | Data unification, Query Federation, identity resolution, segmentation |
| Databricks | Delta Lake tables, Serverless SQL Warehouse, Unity Catalog |
| Agentforce Studio | AI agent design, prompt templates, grounded actions |
SF CLI (sf) |
OAuth browser-flow authentication (no SOAP API) |
| GitHub Actions | Automated data refresh |
-
Query Federation works well. Connecting Databricks to D360 was straightforward -- enter hostname, HTTP path, PAT, done. The "Direct Access (Accelerated)" stream type queries live and caches locally.
-
DMO mapping is the real work. The two-layer model (DLO to DMO) makes architectural sense but the UI-based mapping is manual and tedious. In production, use DevOps Data Kits and
sf project deploy startfor CI/CD. -
Individual-level data is what makes IR work. The original design used company-level web analytics and product usage (keyed by domain). Domain matching produced account-level unification but couldn't match individuals. Switching to
user_emailas the key field -- so each row represents one person's activity, not one company's aggregate -- made Contact Point Email matching work at the individual level. This is the single most important architectural decision in the pipeline. -
Identity Resolution has a steep setup curve. The Individual DMO needs Contact Point Email/Phone objects with formula-based composite primary keys. The Sales Cloud data bundle helps but doesn't auto-configure everything. Understanding the architecture (Individual -> Contact Points via Party ID -> Match Rules) matters more than memorizing the UI steps.
-
CRM-native ingestion is a genuine advantage. Zero-cost, zero-config ingestion from Salesforce objects is something competitors can't match. Building a CDP inside the CRM platform avoids the connector tax that standalone CDPs pay.
-
Agentforce without D360 is limited. An agent that only sees CRM data misses the full picture. D360 is what turns "this account has a deal" into "this account has a deal, declining usage, and 3 escalated tickets."
The biggest lesson was about data granularity and identity resolution.
The first version of this lab used company-level external data: one row per company in web analytics (keyed by domain), one row per company in product usage (keyed by domain). D360's Identity Resolution matched these to accounts via domain, which produced a unified account view -- but not unified individuals.
The problem: D360's IR is designed around the Individual DMO and Contact Point Email. It matches people, not companies. When external data is keyed by domain, there's no email to match against. The IR ruleset can match jane.doe@apexfintech.com (from CRM) to jane.doe@apexfintech.com (from web analytics) -- but only if the web analytics data actually contains that email address.
The fix was to redesign the external data to be individual-level: each row in web analytics and product usage represents one person's activity, keyed by user_email. This meant generating ~44 web analytics rows (one per active contact, excluding role-based emails like info@ and support@) and ~38 product usage rows, rather than the original 20 and 25 company-level rows.
The result: IR now matches a CRM contact to their web analytics record and their product usage record via exact email match through Contact Point Email. Three sources, one unified individual. This is how D360 is designed to work -- the data model just needs to match the platform's assumptions about granularity.
All data is synthetic. No credentials, API keys, or proprietary data from any employer.