Sync custom objects using flows
On this page
StoreConnect automatically syncs its own objects (products, orders, accounts, and others) using change events. For custom objects (those you have defined yourself in Salesforce and mapped via custom data mappings) changes are not tracked automatically. You need to create a Salesforce record-triggered flow that calls the StoreConnect: Sync Record Changes invocable action whenever a record is created, updated, or deleted.
:::note This flow-based sync is different from StoreConnect sync triggers, which are built-in Apex triggers (a global on/off setting) for supported objects. You need this flow precisely because your own custom object is not covered by those built-in triggers. :::
How it works
The SyncRecordChangesInvocable Apex class accepts a record and the type of change (Create, Update, or Delete) and sends the event through StoreConnect’s standard change event pipeline.
Before you begin
Check both of these before building a flow. If either is missing, the flow runs but nothing reaches your store.
- The object is configured in Custom Data Mappings. Changes to an unmapped object are never synced, whatever the flow does.
- StoreConnect Sync is enabled on your org. If sync is disabled, the action returns Success
falsewith the Error MessageStoreConnect Sync disabled, and no change event is generated. This is a separate setting from sync triggers; disabling sync triggers does not disable it.
You need two flows to cover the full record lifecycle: one for creates and updates, and a separate one for deletes. Salesforce cannot handle deletes in the same flow, because a delete flow must run before the record is removed.
:::note
This invocable is designed for custom objects, which bypass per-record opt-in filtering. If you use it on a standard object where per-record opt-in filtering (StoreConnect_Sync__c) is enabled, pass $Record as Entire Resource so the opt-in field is available for evaluation.
:::
Set up a flow for create and update events
- In Salesforce Setup, go to Flows and create a new Record-Triggered Flow.
- Select the custom object you want to sync (for example,
c_o__book__c). - Set the trigger to A record is created or updated.
- Set Optimize the flow for to Actions and Related Records.
- Set the flow to run After the record is saved.
- Add a Decision element. Create an outcome named Create with the condition formula
ISNEW()=True. Leave the default outcome as Update. - For the Create outcome, add an Action element and select StoreConnect: Sync Record Changes. Set the inputs:
- Change Type:
Create - Current Record:
{!$Record}(Entire Resource) - Prior Record:
{!$Record__Prior}(Entire Resource)
- Change Type:
- For the Update (default) outcome, add another Action element and select StoreConnect: Sync Record Changes. Set the inputs:
- Change Type:
Update - Current Record:
{!$Record}(Entire Resource) - Prior Record:
{!$Record__Prior}(Entire Resource)
- Change Type:
- Save and activate the flow.
- Create a record on the object, then query it in a Liquid template or check that it reaches the POS local database, to confirm the change event went through.
Set up a flow for delete events
- Create a new Record-Triggered Flow for the same object.
- Set the trigger to A record is deleted. Salesforce automatically sets Optimize the flow for to Before the record is deleted.
- Add an Action element and select StoreConnect: Sync Record Changes.
- Set the action inputs:
- Change Type:
Delete - Current Record:
{!$Record}(Entire Resource) - Prior Record: leave blank
- Change Type:
- Save and activate the flow.
- Delete a test record and confirm it no longer appears on your store.
A delete can take longer to reach the store than a create, so allow a minute before treating a record that is still visible as a failure.
Backfill records that already exist
The two flows above cover changes made from the moment they are activated. Records that existed before that do not reach your store until you push them, and re-saving them does not do it, for two separate reasons.
The first is that a bare re-save changes nothing. A record whose only changed fields are its last-modified and system-modified stamps generates no change event, so the flow’s action never runs at all.
The second applies even when you do edit a field. The flow sends Change Type Update for any record where ISNEW() is false. An Update targets a row StoreConnect already holds. For a record it has never seen, the store does not write the row. It requests a full refresh of that record from Salesforce instead, so the record arrives only after a round trip, not when the update returns. The action still reports Success true with an empty Error Message, so nothing tells you the record is not there yet. Editing records one at a time is a slow way to backfill, and it gives you no signal when a record has not arrived.
An Update sent without a Prior Record fails visibly instead: the action returns Success false with the Error Message No prior record(s) supplied for update record change type.
Create is the only change type that inserts a row. Sending Create for a record that is already synced does nothing to it: the store neither duplicates the row nor updates it, so the backfill is safe to re-run but does not refresh values that have changed since. A record the store has seen deleted within its retention window, 7 days by default, is not re-inserted either; see the undelete guidance for that case.
Send Create for every existing record once, using anonymous Apex:
```apex
for (Book__c b : [SELECT Id, Name, Author__c FROM Book__c]) { Invocable.Action a = Invocable.Action.createCustomAction( ‘apex’, ‘s_c__SyncRecordChangesInvocable’); a.setInvocationParameter(‘newRecord’, b); a.setInvocationParameter(‘operationType’, ‘Create’); a.invoke(); } ```
The user running this needs Apex class access to s_c__SyncRecordChangesInvocable, the same access the flow’s running user needs.
The action name is the namespaced API name of the invocable class, the same value as actionName in the Flow metadata below.
Each record costs one action invocation, so run the backfill in batches on a large object to stay inside Apex governor limits.
Query the object in a Liquid template afterwards to confirm the rows arrived. If you also need the records on a POS device, see POS sync below.
Action inputs
| Input | Required | Description |
|---|---|---|
| Change Type | Yes | The type of change: Create, Update, or Delete (case-insensitive) |
| Current Record | Yes | The record that triggered the flow. Use {!$Record} (Entire Resource). |
| Prior Record | No | The record before the change. Use {!$Record__Prior} (Entire Resource). Required for updates; leave blank for delete. |
Action outputs
| Output | Type | Description |
|---|---|---|
| Success | Boolean | true if the change event was sent successfully |
| Error Message | Text | Error message if the operation failed |
POS sync
Custom objects that are also referenced in a POS Layout are synced to POS devices. When a change event is generated (either automatically or via this invocable action), the record is pushed to the POS local database on the next sync cycle.
The flows on their own are not enough for the POS. A layout reference is what puts the object in the POS sync schema, so all of the following have to be in place:
- A POS Layout whose Object Name is the object you are syncing. Any Type works, and the layout does not need to be mounted on a screen; it can exist only to register the object.
- A POS Layout Field on that layout for each field you want on the device. Its Field Name must name the same field as the Field API Name on the Custom Data Mapping. See POS layouts.
- The layout reference created before the Custom Data Mapping. Creating a mapping is what triggers the repoll that fetches the new columns, so a mapping that already existed will not backfill them.
:::warning After adding or changing any of this, reload the POS app on each device. The device only rebuilds its data schema when the app boots, so a newly referenced object has no local table until then.
- Browser — refresh the page (
Ctrl+R, orCmd+Ron a Mac). - Native app on Android or iOS — close the app fully and reopen it.
Once the app reloads, the new object re-syncs from the beginning on its own. You do not need to do anything else. :::
Clear & resync will not fix this, and it is the natural thing to reach for. The data sync actions under Settings then Manage data operate on the tables the device already has: they clear records and re-download them. None of them can add a table for an object the device has never seen, so the object stays missing and the configuration looks wrong when it is correct. See Manage POS data for what those actions do, and POS storage, sync, and device administration for how device storage works.
If the object still has no table on the device, check the register’s localStorage. A syncCursors.<object_name> entry whose timestamp tracks your recent record changes, with no matching entry in dbSchema, means the device is receiving the records but has no table to put them in. Reload the POS app to rebuild the schema. If the object is still absent from dbSchema after a reload, gather both values and contact support.
To confirm what actually reached a device, and to tell a missing sync apart from a template problem, see verify custom data is available in Liquid. For how to configure mappings, see custom data mappings, and for reading custom objects in a template, see querying records in Liquid.
Build the flow as metadata
If you deploy the flow as metadata rather than building it in Flow Builder, the input names in the XML differ from the labels shown above: they are operationType, newRecord, and oldRecord.
Both record inputs are generic sObject parameters, so each needs a dataTypeMappings entry naming the object. The delete flow needs a mapping for T__oldRecord as well, even though it never passes that input. Without it the deploy fails with Specify the data type mapping for input parameter T__oldRecord in action s_c__SyncRecordChangesInvocable.
```xml
```
The user running the deploy needs Apex class access to s_c__SyncRecordChangesInvocable. Without it the action cannot be used, and describing it through the REST API returns INSUFFICIENT_ACCESS.
Governor limits
The flow invocable runs as the user who triggered the transaction. Note that although SyncRecordChangesInvocable is declared with sharing, the underlying sync handler executes in system mode, so sharing rules, CRUD, and FLS are not enforced on the sync operations. If you expect high-volume record changes, be aware of Salesforce governor limits on invocable actions in bulk operations. Use asynchronous flow execution where appropriate.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.