SfSkills Domains Docs GitHub

Home / flow / flow-platform-events-integration

flow-platform-events-integration

Use when Flow is a Platform Event publisher, subscriber, or both. Triggers: 'publish platform event from flow', 'platform-event-triggered flow', 'high-volume platform event', 'publish after commit vs immediate', 'PE subscriber error handling', 'integration fan-out from save'. NOT for Change Data Capture — use integration/change-data-capture-integration. NOT for async work that needs no pub/sub — use flow/scheduled-flows.

Open the skill folder on GitHub


Use this skill when Platform Events are the coordination mechanism between Flow and another component — another Flow, Apex, or an external system. Flow can BOTH publish (by creating records on the event sObject) AND subscribe (via a Platform-Event-Triggered Flow). Both sides have transaction-context, delivery-semantics, and error-handling rules that teams commonly get wrong, producing three recurring defect classes: lost events (publisher succeeded, subscriber never ran), replayed events (subscriber ran twice for one logical message), and out-of-order events (two events delivered in the opposite order from publish).

The objective of this skill is to design loosely-coupled, bulk-safe, fault-tolerant Flow-based pub/sub. If the team cannot answer "what happens if the subscriber fails?" and "what happens if the publisher retries?", the design is incomplete.

Before Starting

Check for salesforce-context.md in the project root. If present, read it first.

Gather if not available:

  1. Classify the event type. Decide whether the use case fits Standard-Volume (low-rate, transactional publish-after-commit ordering) or High-Volume (high-rate, Pub/Sub API replay-enabled, no transactional publish-after-commit semantics). Most Flow publishers use Standard-Volume unless the integration target requires high-throughput external consumption.
  2. Design the publisher Flow. Choose the Flow type (record-triggered after-save is the default for "publish on record change"), and choose the publish mode. For record-change events, Publish After Commit prevents phantom events for rolled-back saves.
  3. Design the subscriber Flow. Create a Platform-Event-Triggered flow, define the entry condition, and plan for idempotency: the same event may be delivered more than once under failure, especially for High-Volume.
  4. Document fan-out. If multiple subscribers consume the event (Flow + Apex trigger + external), list all of them. Each has independent failure semantics.
  5. Plan error handling. Publisher failures (publish-limit exhaustion, field validation) must route to a fault log. Subscriber failures do NOT roll back the publisher. Plan compensation.
  6. Compute budget. Publish counts against per-transaction DML. Subscriber flows consume their own async budget per delivery batch.
  7. Validate. Run python3 scripts/skill_sync.py --skill skills/flow/flow-platform-events-integration and the repo-level validator.

Core Concepts

Standard-Volume vs High-Volume Platform Events

AttributeStandard-VolumeHigh-Volume
Publish rate (per hour)100,000 org-wide (Enterprise / Performance / Unlimited); 1,000 (Developer, and Professional with API Add-On)250,000 org-wide (Enterprise / Performance / Unlimited / Professional with API Add-On); 50,000 (Developer)
Publish-after-commit semanticsYes — only delivers on successful commitEvent is delivered on publish, not tied to DML commit ordering in the same way
Replay / durabilityShort-lived bufferDurable 72-hour event bus with replay via replayId
External consumptionLimitedDesigned for Pub/Sub API external consumers
Best forInternal Flow-to-Flow or Flow-to-Apex coordinationIntegration fan-out, external systems, high-throughput telemetry

All NEW Platform Event object definitions default to High-Volume; only legacy events are Standard-Volume. Check the object definition's isStandardVolume attribute / Setup page.

Publish Immediately vs Publish After Commit

A record-triggered flow that creates a Price_Change__e event can choose:

In Flow Builder, the Create Records element on a Platform Event object exposes a "How to Run the Flow After Save" option that controls publish semantics at the flow level (for record-triggered flows). For autolaunched publishers, you set the immediate/after-commit intent at event definition time (some event types only support one mode).

Subscribing With Platform-Event-Triggered Flows

A Platform-Event-Triggered flow activates when a message is delivered on its event channel. Key properties:

Delivery Semantics

Event typeDelivery guaranteeReplay
Standard-Volume PEAt-least-once within a short windowNot available
High-Volume PEAt-least-once, durable 72hYes via replayId in Pub/Sub API

"At-least-once" means a subscriber MAY be invoked more than once per logical event. Idempotency is the subscriber's responsibility.

Coordinating Flow and Apex Subscribers

Both a Platform-Event-Triggered flow AND an apex trigger ... on Event__e (after insert) can coexist on the same event. They are independent subscribers. Each has its own transaction, governor budget, and failure handling. Running order between Flow and Apex subscribers is NOT guaranteed. Do not design a sequence where the Flow subscriber must run before the Apex subscriber.

Key Patterns

Pattern 1: Publish an Event from a Record-Triggered Flow With After-Commit Safety

When to use: On a Case save, notify downstream systems that a case status changed. You do NOT want to notify if the save rolls back.

Pseudo-flow (After-Save on Case):

[Start: Record-Triggered, After-Save, Create/Update on Case]
  Entry condition: ISCHANGED([Case].Status)
  └── [Create Records: new Case_Status_Changed__e]
         caseId__c  = {!$Record.Id}
         oldStatus__c = {!$Record__Prior.Status}
         newStatus__c = {!$Record.Status}
         changedAt__c = {!$Flow.CurrentDateTime}
  └── [End]

Metadata (publisher):

<triggerType>RecordAfterSave</triggerType>
<recordCreates>
  <name>Publish_Status_Change</name>
  <object>Case_Status_Changed__e</object>
  <inputAssignments>
    <field>caseId__c</field>
    <value><elementReference>$Record.Id</elementReference></value>
  </inputAssignments>
  <inputAssignments>
    <field>newStatus__c</field>
    <value><elementReference>$Record.Status</elementReference></value>
  </inputAssignments>
</recordCreates>

Why it works: The Create Records on the event object is the publish action. Because the flow is After-Save and the event supports publish-after-commit, the event is buffered until the current DML commits. If the save rolls back, the event is discarded. No phantom notifications.

Caveat: Standard-Volume publishing is allocated 100,000 events per hour org-wide on Enterprise / Performance / Unlimited (1,000 on Developer and Professional-with-API-Add-On), and that bucket is shared across every Standard-Volume event type in the org. A 100,000-record bulk import that publishes one event per record consumes the entire hourly allocation on its own, leaving nothing for any other Standard-Volume publisher — and on Developer or Professional it fails almost immediately. For that cardinality, switch to High-Volume (250,000/hour) or batch the publish. Note that High-Volume publishing is also org-wide, not a fresh allocation per event type.

Pattern 2: Subscribe to an Event With a Platform-Event-Triggered Flow

When to use: On Case_Status_Changed__e, create an Audit_Entry__c row and update a summary on Account.

Pseudo-flow (Platform-Event-Triggered):

[Start: Platform-Event-Triggered on Case_Status_Changed__e]
  └── [Get Records: Case WHERE Id = {!$Record.caseId__c}]
  └── [Decision: Case found?]
        ├── Yes → [Create Records: new Audit_Entry__c ...]
        │        [Get Records: Account WHERE Id = {!Case.AccountId}]
        │        [Update Records: Account.LastCaseChange__c = {!$Record.changedAt__c}]
        └── No  → [Create Records: Integration_Error_Log__c (case not found)]
[End]

Metadata (subscriber):

<triggerType>PlatformEvent</triggerType>
<start>
  <object>Case_Status_Changed__e</object>
</start>
<recordLookups>
  <name>Lookup_Case</name>
  <object>Case</object>
  <filters>
    <field>Id</field>
    <operator>EqualTo</operator>
    <value><elementReference>$Record.caseId__c</elementReference></value>
  </filters>
</recordLookups>

Why it works: The subscriber runs in its own async transaction. It does not block or interact with the publisher. It has async governor limits and isolated failure.

Idempotency note: At-least-once delivery means this flow may run twice for the same event. Before writing the Audit_Entry__c, check for an existing entry with the same (caseId, changedAt) tuple. The Audit_Entry__c should have a unique index on (Case__c, ChangedAt__c) to enforce this.

Pattern 3: Fan-Out With High-Volume Events

When to use: An order is placed; downstream fulfillment, billing, and analytics must each receive the event. The rate is > 10,000 events/hour.

Topology:

[Order After-Save] --> publishes [Order_Placed__e (High-Volume)]
                                    |
               +--------------------+---------------------+
               |                    |                     |
  [Flow subscriber]     [Apex @Triggered trigger]   [External Pub/Sub API consumer]
  (creates ShipRequest  (posts to internal          (MuleSoft/Kafka bridge for
   record)               billing queue)              data lake)

Design rules:

Pattern 4: Publisher Budget Guard

When to use: A bulk-save path may publish hundreds of events in one transaction.

Risk: Publishing too many events in one transaction hits the per-transaction DML limit (the Create Records on the event object counts as DML). Additionally, the org-wide publish-per-hour limit can be exhausted for Standard-Volume events.

Mitigation:

[After-Save Flow]
  └── [Get Records: count $Record collection size]
  └── [Decision: collection size > 100]
        ├── Yes → [Create Records: Batch_Publish_Request__c row per 100]
        │          (a Scheduled Path sweeps Batch_Publish_Request__c rows in smaller batches)
        └── No  → [Create Records: inline PE publish]

Alternative: switch the event to High-Volume if the workload demands steady-state high rates.

Pattern 5: Deliberate Failure Handling in Subscriber

When to use: Every subscriber. Not optional.

Pseudo-flow:

[Platform-Event-Triggered on Case_Status_Changed__e]
  └── [Get Records: Case] (with Fault Path)
        ├── Happy → continue
        └── Fault → [Create Records: PE_Subscriber_Error_Log__c
                        event_type__c = 'Case_Status_Changed__e'
                        payload_json__c = <serialized $Record>
                        error__c = {!$Flow.FaultMessage}]
                    [Send Custom Notification to Admin Queue]
                    [End] (swallow; platform does not re-deliver on Flow fault)

Key insight: A Flow subscriber fault does NOT cause redelivery. The platform considers the delivery complete. If you swallow the error silently, the business message is lost. Always log to a durable store and notify.

Bulk safety

Both publish and subscribe have bulk semantics.

Error handling

Well-Architected mapping

Review Checklist

Salesforce-Specific Gotchas (short list)

  1. Both Standard-Volume and High-Volume publish allocations are ORG-WIDE, not per-event-type — adding a new event type buys no fresh headroom. Standard-Volume: 100,000/hour (Enterprise / Performance / Unlimited), 1,000/hour (Developer, Professional with API Add-On). High-Volume: 250,000/hour (50,000 on Developer).
  2. Publish Immediately can deliver events that should have rolled back; always prefer after-commit for record-change triggers.
  3. Subscriber flows run as Automated Process user unless explicitly configured.
  4. At-least-once delivery means idempotency is mandatory.

See references/gotchas.md for the full set.

Official Sources Used

Package files