React to Data 360 Changes from Apex and Flow

Data actions, platform events and Data 360–triggered flows — how insight in the lakehouse becomes action in the org, built around a churn-risk Case.

The scenario

Northbay Goods is an e-commerce retailer. Following the pattern from Part 2, their data team built a churn-risk Calculated Insight in Data 360: order frequency, days since last session, return rate and support sentiment roll up into a 0–100 score per unified customer, refreshed nightly.

The score works. The problem is what happens next: nothing. The list sits in a dashboard someone checks on Fridays. By the time a human notices a VIP customer crossed the line, the win-back window has closed.

The retention team's requirement: "When a customer's score crosses 80, open a Case for the retention queue that day. One Case — not one per nightly refresh." That's an integration problem, and it's the part of Data 360 that belongs squarely to platform developers: getting changes in the lakehouse to trigger automation in the org.

Three ways out of the lakehouse

Data 360 has three mechanisms for pushing changes toward org automation. Pick by who maintains it and how much logic sits behind the trigger:

MechanismWhat it doesReach for it when
Data action → platform eventA rule on a DMO or Calculated Insight publishes a custom platform event when conditions matchYou want Apex — bulkified logic, dedupe guards, real unit tests
Data 360–triggered flowA flow launches when records in a DMO or CI change (labeled Data Cloud-Triggered Flow in Flow Builder)Admin-maintained, declarative-first orgs; simpler follow-on actions
Standard change eventDataObjectDataChgEvent notifies subscribers of data changes in Data 360Generic listeners and integration middleware watching broadly

Northbay wants a dedupe guard ("one open Case per customer") and a test suite the release pipeline can run. That's Apex logic, so the main build is path one. We'll cover the flow variant at the end — same trigger, different consumer.

Step 1 — define the platform event

In Setup → Platform Events, create Churn_Risk_Flagged__e with three custom fields:

  • Contact_Email__c (Text) — the key to find the CRM-side customer.
  • Churn_Score__c (Number) — the score that tripped the rule.
  • Unified_Customer_Id__c (Text) — the Data 360 profile ID, kept for traceability.

Design the event like an API contract, because it is one: Data 360 is the producer, your org is the consumer, and every field is something the consumer would otherwise have to query back for.

Step 2 — the data action

In Data 360:

  1. Data Action Targets → New → type Salesforce Platform Event → pick Churn_Risk_Flagged__e.
  2. Data Actions → New → source: the churn-risk Calculated Insight.
  3. Condition: churn_score__c >= 80. Map the CI's dimensions and measures onto the event fields.

From now on, every nightly CI refresh evaluates the rule and publishes one event per newly matching customer. Note the semantics: data actions fire on changes in the result, so a customer sitting at 85 for a week doesn't re-fire every night — but score oscillation around the threshold (79, 81, 79, 82…) can. That's one reason the subscriber still needs its own dedupe guard.

Step 3 — the Apex subscriber

A platform event trigger, thin as always, handing off to a service class:

trigger ChurnRiskFlagged on Churn_Risk_Flagged__e (after insert) {
    ChurnRiskCaseService.createRetentionCases(Trigger.new);
}

The service does the real work: resolve the contact, enforce "one open Case per customer", insert in bulk.

public with sharing class ChurnRiskCaseService {

    public static void createRetentionCases(List<Churn_Risk_Flagged__e> events) {
        Set<String> emails = new Set<String>();
        for (Churn_Risk_Flagged__e evt : events) {
            if (String.isNotBlank(evt.Contact_Email__c)) {
                emails.add(evt.Contact_Email__c.toLowerCase());
            }
        }
        if (emails.isEmpty()) { return; }

        Map<String, Id> contactByEmail = new Map<String, Id>();
        for (Contact c : [
            SELECT Id, Email FROM Contact WHERE Email IN :emails
        ]) {
            contactByEmail.put(c.Email.toLowerCase(), c.Id);
        }

        // Dedupe guard: customers who already have an open retention case
        Set<Id> alreadyFlagged = new Set<Id>();
        for (Case existing : [
            SELECT ContactId FROM Case
            WHERE ContactId IN :contactByEmail.values()
              AND Subject LIKE 'Churn risk:%'
              AND IsClosed = false
        ]) {
            alreadyFlagged.add(existing.ContactId);
        }

        List<Case> retentionCases = new List<Case>();
        for (Churn_Risk_Flagged__e evt : events) {
            Id contactId = String.isBlank(evt.Contact_Email__c)
                ? null
                : contactByEmail.get(evt.Contact_Email__c.toLowerCase());
            if (contactId == null || alreadyFlagged.contains(contactId)) {
                continue;
            }
            alreadyFlagged.add(contactId); // dedupe within this batch too
            retentionCases.add(new Case(
                ContactId   = contactId,
                Subject     = 'Churn risk: score ' + evt.Churn_Score__c.intValue(),
                Description = 'Flagged by the Data 360 churn-risk Calculated Insight. ' +
                              'Unified profile: ' + evt.Unified_Customer_Id__c,
                Priority    = 'High'
            ));
        }
        if (!retentionCases.isEmpty()) {
            insert retentionCases;
        }
    }
}

Route the Cases with standard assignment rules on the subject prefix, or set an OwnerId to the retention queue directly. If a downstream agent or automation should act on these Cases and write back, put it behind the confirmation guardrails from our governed agent actions build.

Platform event triggers have their own rules: they run as the Automated Process user (set Case ownership deliberately), retries replay the whole batch (keep the handler idempotent — the dedupe guard does double duty here), and there's no after undelete to worry about. Idempotency isn't polish; it's the difference between one Case and five.

Step 4 — prove it with a test

Test.stopTest() delivers published platform events, so the whole path is unit-testable:

@IsTest
private class ChurnRiskCaseServiceTest {

    @IsTest
    static void createsOneCaseAndDedupes() {
        Contact maya = new Contact(LastName = 'Iyer', Email = 'maya@example.com');
        insert maya;

        Churn_Risk_Flagged__e evt = new Churn_Risk_Flagged__e(
            Contact_Email__c       = 'maya@example.com',
            Churn_Score__c         = 91,
            Unified_Customer_Id__c = 'UI-000482'
        );

        Test.startTest();
        EventBus.publish(evt);
        EventBus.publish(evt); // same customer flagged twice
        Test.stopTest();

        System.assertEquals(1,
            [SELECT COUNT() FROM Case WHERE ContactId = :maya.Id],
            'Duplicate events must not create duplicate cases');
    }
}

That second publish is the whole point of the test: it encodes the retention team's "one Case, not one per refresh" requirement as an assertion the CI pipeline enforces forever.

The declarative variant: Data 360–triggered flow

If Northbay's admins owned this requirement instead, the same trigger point works without the event or the Apex: create a Data 360–triggered flow, point it at the churn-risk CI object, and set the entry condition to churn_score__c >= 80. The flow then runs Get Records on Contact by email and creates the Case — with a Get Records + Decision pair standing in for the dedupe query.

When to prefer which:

  • Flow: the logic fits in a handful of elements, admins maintain it, and per-record processing is acceptable.
  • Apex via platform event: you need bulk-safe queries, cross-object dedupe, unit tests, or the event has multiple consumers (Case creation today, a Slack alert tomorrow — same event, another subscriber, zero changes in Data 360).

That last point is the quiet architectural win of the event route: the platform event decouples Data 360 from however many things the org decides to do about churn next year.

What you learned

  • Insight without automation is a dashboard nobody reads in time — data actions are how Data 360 initiates action.
  • Data actions publish custom platform events when DMO or Calculated Insight conditions match; design the event like an API contract.
  • Apex subscribers give you bulkification, dedupe guards and real tests; Test.stopTest() delivers events in unit tests.
  • Platform event triggers run as the Automated Process user and may replay batches — build idempotent handlers.
  • Data 360–triggered flows cover the declarative cases; the platform event wins when logic or consumer count grows.

Next in the series: Data 360 with Tableau and CRM Analytics — the same unified data, now for the executives. The full series lives on the Data 360 track page.

Sources: Salesforce Help — Data Actions in Data 360 · Platform Events Developer Guide — DataObjectDataChgEvent · Salesforce Developers — Data Cloud-Triggered Flows and Invocable Actions