Sync Component
Introductionβ
The Sync component is a non-visual SDUI component that enables offline-first capabilities for mobile applications. It defines what data to pre-load and cache locally, how to queue operations (workflow executions, file uploads) when offline, and how to synchronize changes when connectivity is restored.
The Sync component acts as a data layer adapter β other SDUI components (datasource, form, button) interact with it transparently, without needing to know whether data comes from the network or local cache.
Key Capabilitiesβ
- Data pre-loading: Cache configurable datasets for offline access
- Operation queuing: Persist workflow executions and mutations when offline
- Background sync: Process queued operations automatically via expo-task-manager
- Attachment uploads: Queue and upload files in the background with compression
- Status visibility: Expose sync state for UI indicators and an operations screen
- Conflict resolution: Last-write-wins strategy for offline changes (see Conflict Resolution)
Design Principlesβ
- Declarative: Sync behavior is defined in YAML, consistent with the SDUI pattern
- Transparent: Consuming components don't distinguish between online and offline
- Resilient: Queue persists across app restarts and survives app kills
- Configurable: Each workflow and model can specify its own sync strategy
YAML Structureβ
component: sync
name: <syncInstanceName>
props:
models:
- name: <string>
query: <graphql>
variables: <object>
keys: [<string>]
syncInterval: <number>
syncOn: [<trigger>]
maxAge: <number>
queue:
maxRetries: <number>
retryInterval: <number>
retryStrategy: <strategy>
persistQueue: <boolean>
backgroundSync: <boolean>
backgroundInterval: <number>
maxQueueSize: <number>
workflows:
default: <mode>
items:
- workflowId: <string>
mode: <mode>
optimisticResult: <object>
rollbackAction: <action[]>
dependsOn: [<string>]
attachments:
strategy: <strategy>
maxConcurrent: <number>
retryOnFailure: <boolean>
compress:
enabled: <boolean>
quality: <number>
maxDimension: <number>
status:
showIndicator: <boolean>
position: <position>
showPendingCount: <boolean>
notifyOnComplete: <boolean>
notifyOnError: <boolean>
Attribute Descriptionβ
Top-Level Propsβ
| Attribute | Type | Required | Description |
|---|---|---|---|
models | array | No | List of data models to pre-load and cache for offline access |
queue | object | No | Configuration for the offline operation queue |
workflows | object | No | Workflow execution mode configuration (queue vs optimistic) |
attachments | object | No | File upload queue configuration |
status | object | No | Sync status UI indicator configuration |
Modelsβ
Each model defines a dataset to cache locally for offline access.
| Attribute | Type | Default | Description |
|---|---|---|---|
name | string | β | Required. Unique identifier for this model within the sync instance |
query | string | β | Required. GraphQL query string to fetch the data. Must return a single root field containing an items array (or a root-level array) β see Model Query Shape |
variables | object | {} | Template-parsed variables passed to the query. Supports {{ }} expressions β global scope only, see Variable Scope |
keys | string[] | β | Required. Primary key field(s) for cache identity and deduplication |
syncInterval | number | 300 | Seconds between automatic background refreshes when online. Ignored when syncOn is [manual] |
syncOn | string[] | ["networkReconnect"] | Additional triggers for syncing this model |
maxAge | number | 86400 | Maximum seconds before cached data is considered stale (default: 24h) |
filter | string | β | Optional default filter applied to cached results |
maxItems | number | 1000 | Maximum number of items to cache for this model |
syncOn Triggersβ
| Value | Description |
|---|---|
appForeground | Sync when the app returns to the foreground |
networkReconnect | Sync when network connectivity is restored |
manual | Only sync when explicitly triggered via the syncNow/syncModel adapter actions or the refresh action. Must be the only trigger listed; suppresses syncInterval |
intervalOnly | Only sync on the defined syncInterval, no event-based triggers |
Model Query Shapeβ
The engine extracts cached items from the query result by convention: the
response must contain a single root field whose value either has an
items array (the standard paginated list shape) or is itself an array.
Results that do not match this shape cache nothing and log a warning.
Model Variable Scopeβ
Model variables and the model-level filter are evaluated when the engine
syncs in the background β timers, reconnect, and app-foreground triggers run
with no screen open. Expressions must therefore resolve from the global
scope only: organizationId, user.* / currentUser.*, and global
variables. Screen- or form-scoped expressions in model definitions are
undefined behavior. (Workflow inputs are unaffected β they are fully
resolved at submit time, while the screen still exists.)
Queueβ
Configuration for the offline operation queue that persists and processes mutations/workflows.
| Attribute | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 5 | Maximum retry attempts for a failed operation |
retryInterval | number | 30 | Base retry delay in seconds |
retryStrategy | string | "exponential" | Backoff strategy: "exponential" or "fixed" |
persistQueue | boolean | true | Whether the queue persists to device storage (survives app restart) |
backgroundSync | boolean | true | Whether to use expo-task-manager for background queue processing. Currently a no-op β see note below |
backgroundInterval | number | 60 | Minimum seconds between background sync attempts |
True background execution (app killed or backgrounded) requires
expo-task-manager, which is not yet enabled in the mobile app.
backgroundSync: true is accepted but currently a no-op: the queue processes
on network reconnect, app foreground, interval timers, and manual syncNow β
all while the app is running. Queued operations always survive restarts and
process on the next launch.
| maxQueueSize | number | 100 | Maximum pending operations. When the queue is full, new offline operations fail immediately and the action's onError fires |
Operation Identity and Replay Safetyβ
Every operation is stamped with a client-generated operation ID (GUID) when it enters the queue. Workflow operations send this ID as the executionId of the executeWorkflow call on every attempt, and the server deduplicates on it: if an earlier attempt actually succeeded β for example, the request timed out after the server had already accepted it β the retry returns the original execution instead of running the workflow again. This is what makes the retry policy above safe: an ambiguous network failure can never double-execute a business operation.
Failure Classificationβ
Not every failure is retried. The queue classifies errors as:
| Class | Examples | Behavior |
|---|---|---|
| Transient | Network errors, timeouts, HTTP 5xx | Retried per retryStrategy up to maxRetries, then treated as permanent |
| Permanent | Workflow validation errors, business rule rejections, HTTP 4xx | Fails immediately β no retries. Fires onSyncFailed, then rollbackAction |
| Authentication | Expired or invalid token (HTTP 401) | Queue pauses until the user re-authenticates; does not consume retry attempts |
Permanently failed operations remain visible in the Sync Operations screen, where the user can inspect, retry, or discard them.
Workflowsβ
Defines how each workflow action behaves when executed offline.
| Attribute | Type | Default | Description |
|---|---|---|---|
default | string | "queue" | Default mode for workflows not explicitly listed. Only "queue" may be used as the default β "optimistic" requires per-workflow targetModel/targetKey |
items | array | [] | Per-workflow configuration overrides |
Workflow Itemβ
| Attribute | Type | Default | Description |
|---|---|---|---|
workflowId | string | β | Required. The workflow identifier to configure |
mode | string | inherited | Execution mode: "queue" or "optimistic" |
targetModel | string | β | Required in optimistic mode. The cached model this workflow updates |
targetKey | string | β | Required in optimistic mode. Template expression, evaluated against the workflow inputs, that resolves the key of the cached record to patch (e.g., "{{ inputs.orderId }}") |
optimisticResult | object | β | Fields shallow-merged into the targeted cached record when in optimistic mode. Must match the cached record's shape |
rollbackAction | action[] | β | Actions executed if the operation fails permanently after all retries |
dependsOn | string[] | β | Queue item types that must complete before this workflow executes. upload waits for the attachments referenced in this operation's inputs, not all pending uploads |
Execution Modesβ
queue: The workflow call (workflowId + inputs) is stored in the queue. It executes server-side when connectivity is available. The user receives a "queued" confirmation but no immediate result.optimistic: The engine locates the record intargetModelwhose key matches the evaluatedtargetKey, snapshots it, and shallow-mergesoptimisticResultinto it immediately β the user sees the change instantly. The actual workflow executes when connectivity is available. If it fails permanently, the snapshot is restored androllbackActionruns. If no cached record matchestargetKey, the operation falls back toqueuemode.
Optimistic mode only updates existing cached records. Workflows that create new entities must use queue mode β the server assigns identity and computed fields that the client cannot predict.
Attachmentsβ
Configuration for the file upload queue.
| Attribute | Type | Default | Description |
|---|---|---|---|
strategy | string | "background" | Upload strategy: "background" (queue for later) or "immediate" (block until done) |
maxConcurrent | number | 2 | Maximum simultaneous upload operations |
retryOnFailure | boolean | true | Whether failed uploads are retried automatically |
compress.enabled | boolean | false | Whether to compress images before upload |
compress.quality | number | 0.8 | Image compression quality (0.0β1.0) |
compress.maxDimension | number | 2048 | Maximum width or height in pixels (aspect ratio preserved) |
Offline Uploads and Attachment IDsβ
Attachment fields opt into the sync queue explicitly via options.syncAdapter:
- component: field
name: photos
props:
type: attachment
options:
syncAdapter: inspectionSync
allowMultiple: true
Attachment identity is client-generated: when a file is selected, the field immediately generates a GUID that becomes the permanent attachmentId β whether the upload happens now or hours later. The GUID is returned to the form right away, so workflow inputs can reference it before the file has ever reached the server; no placeholder rewriting is needed. When the queued upload completes, the server stores the attachment record under the same client-supplied GUID.
Because the GUID only becomes resolvable once the file exists server-side, workflows whose inputs reference attachments must declare dependsOn: [upload] so they execute only after their own files have finished uploading.
Server-side, an attachment referenced by GUID before its file has arrived is stored with status PendingUpload; it becomes Active when the queued upload completes, or UploadFailed if the upload fails permanently. Pending attachments are excluded from normal attachment queries, so half-synced files never appear in document listings. The Sync Operations screen surfaces these statuses alongside the queue items.
Statusβ
Configuration for the sync status UI indicator.
| Attribute | Type | Default | Description |
|---|---|---|---|
showIndicator | boolean | true | Whether to show a sync status badge/icon |
position | string | "header" | Where to display the indicator: "top", "bottom", or "header" |
showPendingCount | boolean | true | Whether to show the count of pending operations as a badge |
notifyOnComplete | boolean | true | Show notification when all queued operations sync successfully |
notifyOnError | boolean | true | Show notification when an operation fails permanently |
Lifecycle and Placementβ
A sync instance is registered globally on first render and lives for the rest of the app session β it is not tied to the screen that declared it:
- Place the sync component in the root screen of the module whose data it manages (or in a shared module for global data). It registers when the user first opens that screen.
- Navigating away does not stop syncing, clear the cache, or pause the queue. The sync engine, queue processor, and background worker are app-wide singletons serving all registered instances.
- Re-rendering a sync component whose
nameis already registered updates that instance's configuration in place (idempotent re-registration). Two different sync instances must not share a name. - Cached data and queues are namespaced per sync instance (
syncName/modelName) and scoped to the current organization. Switching organizations switches to a separate cache namespace; the previous organization's cache and queue are preserved and resume processing when the user switches back. - Queued operations survive app restarts (
persistQueue: true) and are processed even if the originating module's screen is never reopened.
Adapter Interfaceβ
Other SDUI components interact with the Sync component via the syncAdapter prop. This provides transparent offline access without the consuming component needing to handle online/offline logic.
Usage in DataSourceβ
- component: datasource
name: vehicleData
props:
name: vehicleData
queries:
- name: getVehicles
query:
syncAdapter: receivingSync
model: vehicles
filter: "trackingNumber:{{ trackingNumber }}"
When a query specifies syncAdapter, the datasource reads that query's result from the local cache instead of the network. If online and the cached data is stale (or was never fetched), fresh data is fetched in the background and re-delivered to the screen automatically when it lands (stale-while-revalidate). The syncAdapter/model/filter combination is a third query source alongside the existing command and workflow forms, and the result is stored under the query name as usual.
Read Result Shapeβ
A sync-adapter query stores this shape under the query name:
{
"items": [],
"totalCount": 0,
"fetchedAt": "2026-07-03T10:15:00.000Z",
"isStale": false
}
Consuming components bind the item list explicitly β e.g. {{ getVehicles.items }} for a collection, or {{ getVehicles.items.0 }} for a single record looked up by filter. fetchedAt is null and isStale is true when the model has never been synced (prime models online first).
Filter Grammarβ
The filter string is template-resolved first, then evaluated client-side against the cached items:
- Terms have the form
fieldPath:value; the path supports dot notation (orderStatus.orderStatusName:Received) and splits on the first colon only. - Multiple terms combine with
AND(uppercase). There is noORand no comparison operators in this version. - Matching uses loose string equality (
orderId:42matches the number42). - A term whose value resolves to empty (missing template variable) matches no items β predictable behavior while route params are not yet available.
Usage in Workflow Actionsβ
- component: button
props:
label:
en-US: Submit
onClick:
- workflow:
workflowId: "6bda41d8-..."
inputs:
data: "{{ form.values }}"
syncAdapter: receivingSync
onQueued:
- notification:
message:
en-US: "Saved offline. Will sync when connected."
variant: info
onSynced:
- notification:
message:
en-US: "Successfully synced to server."
variant: success
Adapter Actionsβ
The sync adapter exposes the following actions that can be triggered from YAML:
| Action | Description |
|---|---|
syncNow | Force immediate sync of all models and process queue |
syncModel | Force sync of a specific model by name |
clearCache | Clear all cached data for this sync instance |
clearQueue | Discard all pending queue operations |
retryFailed | Retry all permanently failed operations |
- component: button
props:
label:
en-US: Refresh Data
onClick:
- syncAction:
adapter: receivingSync
action: syncModel
model: vehicles
New Action Eventsβ
When syncAdapter is specified on a workflow action, additional events become available:
| Event | Description |
|---|---|
onQueued | Fired when the operation is added to the offline queue (offline path) |
onSynced | Fired when a previously queued operation completes successfully |
onSyncFailed | Fired when a queued operation fails permanently after all retries |
Interplay with onSuccess / onErrorβ
The existing onSuccess and onError events of the workflow action keep their current behavior when the device is online. The sync events cover the offline path:
| Scenario | Events fired |
|---|---|
| Online β workflow executes immediately | onSuccess or onError (unchanged) |
Offline β operation added to the queue (queue or optimistic mode) | onQueued, immediately |
| Queued operation later syncs successfully | onSynced |
| Queued operation fails permanently after all retries | onSyncFailed, then rollbackAction (optimistic mode) |
onSuccess and onError never fire for queued executions β by the time the queue is processed, the screen that initiated the action may no longer be open. Use onQueued for immediate user feedback and onSynced/onSyncFailed (or the status indicator and notifications) for post-sync feedback.
Examplesβ
Basic: Cache a Single Modelβ
Pre-load customer contacts for offline access with refresh every 10 minutes:
component: sync
name: contactsSync
props:
models:
- name: customers
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, filter: "contactType:Customer") {
items {
contactId
name
email
phone
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]
Intermediate: Multiple Models with Workflow Queueβ
A receiving module that caches vehicles and locations, with offline workflow support:
component: sync
name: receivingSync
props:
models:
- name: vehicles
query: |
query($organizationId: Int!, $filter: String) {
orders(organizationId: $organizationId, filter: $filter, limit: 500) {
items {
orderId
trackingNumber
orderStatus { orderStatusName }
orderCommodities {
commodity { customValues }
}
billToContact { contactId name }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
filter: "orderType:Freight"
keys: [orderId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]
- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items {
warehouseLocationId
name
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600
queue:
maxRetries: 5
retryStrategy: exponential
backgroundSync: true
backgroundInterval: 60
workflows:
default: queue
items:
- workflowId: "6bda41d8-28af-42ac-9975-0f7adc0dc45e"
mode: optimistic
targetModel: vehicles
targetKey: "{{ inputs.orderId }}"
optimisticResult:
orderStatus:
orderStatusName: Received
rollbackAction:
- notification:
message:
en-US: "Receiving failed to sync. Check Sync Operations."
variant: error
dependsOn: [upload]
- workflowId: "2ff3c10b-d2d9-418f-a41a-8834a58abf2e"
mode: queue
Advanced: Full Offline Module with Attachmentsβ
Complete sync configuration for a field inspection module with photo capture and background upload:
component: sync
name: inspectionSync
props:
models:
- name: assignments
query: |
query($organizationId: Int!, $filter: String) {
inspectionAssignments(
organizationId: $organizationId
filter: $filter
) {
items {
assignmentId
location { name address }
scheduledDate
inspectionType
status
notes
}
}
}
variables:
organizationId: "{{ number organizationId }}"
filter: "assignedTo:{{ user.userId }} AND status:Pending"
keys: [assignmentId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]
maxItems: 200
- name: inspectionTemplates
query: |
query($organizationId: Int!) {
inspectionTemplates(organizationId: $organizationId) {
items {
templateId
name
sections { sectionId title fields { fieldId label type required } }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [templateId]
syncOn: [manual]
queue:
maxRetries: 5
retryStrategy: exponential
retryInterval: 30
backgroundSync: true
backgroundInterval: 60
maxQueueSize: 200
persistQueue: true
workflows:
default: queue
items:
- workflowId: "a1b2c3d4-complete-inspection"
mode: optimistic
targetModel: assignments
targetKey: "{{ inputs.assignmentId }}"
optimisticResult:
status: Completed
rollbackAction:
- notification:
message:
en-US: "Inspection submission failed. Your data is saved locally."
variant: warning
dependsOn: [upload]
attachments:
strategy: background
maxConcurrent: 2
retryOnFailure: true
compress:
enabled: true
quality: 0.8
maxDimension: 2048
status:
showIndicator: true
position: header
showPendingCount: true
notifyOnComplete: true
notifyOnError: true
Using the Adapter in a Formβ
How other components consume the sync adapter for seamless offline operation:
- component: form
name: inspectionForm
props:
validationSchema: {}
children:
# Load assignment data from cache
- component: datasource
name: assignmentData
props:
name: assignmentData
queries:
- name: getAssignment
query:
syncAdapter: inspectionSync
model: assignments
filter: "assignmentId:{{ assignmentId }}"
# Form fields...
- component: field
name: notes
props:
type: text
label:
en-US: Inspection Notes
# Photo attachment with offline queue
- component: field
name: photos
props:
type: attachment
label:
en-US: Take Photos
options:
syncAdapter: inspectionSync
allowMultiple: true
displayAs: image
parentType: Inspection
parentId: "{{ assignmentId }}"
# Submit with offline support
- component: button
name: submitInspection
props:
label:
en-US: Complete Inspection
options:
variant: primary
onClick:
- validateForm:
validationSchema:
notes:
required:
message: Notes are required
- workflow:
workflowId: "a1b2c3d4-complete-inspection"
inputs:
assignmentId: "{{ assignmentId }}"
notes: "{{ inspectionForm.notes }}"
photos: "{{ inspectionForm.photos }}"
syncAdapter: inspectionSync
onQueued:
- notification:
message:
en-US: "Inspection saved. Will sync when online."
variant: info
- navigateBackOrClose:
onSynced:
- notification:
message:
en-US: "Inspection synced successfully."
variant: success
Global Sync (Shared Module)β
A sync component defined in a shared module, caching data used across all modules:
component: sync
name: globalSync
props:
models:
- name: contacts
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, limit: 1000) {
items { contactId name contactType email phone }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]
- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items { warehouseLocationId name code }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600
- name: users
query: |
query($organizationId: Int!) {
users(organizationId: $organizationId) {
items { userId displayName email }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [userId]
syncInterval: 3600
syncOn: [appForeground]
status:
showIndicator: true
position: header
showPendingCount: false
Sync Operations Screenβ
The Sync component automatically registers a built-in screen at the route sync-operations that displays all sync instances, their cached models, and the operation queue.
Screen Sectionsβ
- Status Bar: Current network state (Online/Offline), last sync timestamp, manual Sync Now button
- Sync Instances: Grouped by sync name, showing:
- Each cached model with item count and freshness indicator
- Pending queue items with operation type, timestamp, and retry count
- Queue Actions: Per-item Retry and Discard buttons
- History: Completed and failed operations with timestamps
Accessing the Screenβ
- component: button
props:
label:
en-US: View Sync Status
onClick:
- navigate: "sync-operations"
The screen is also accessible via the sync status indicator when tapped.
Architectureβ
Data Flowβ
βββββββββββββββββββββββββββββββββββββββββββ
β YAML Sync Definition β
β (models, queue, workflows, attachments) β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β parsed at module load
ββββββββββββββββββββΌβββββββββββββββββββββββ
β Sync Engine β
β ββββββββββββββ ββββββββββββββββββββ β
β βModel Cache β β Operation Queue β β
β β(persisted) β β (Zustand+persist)β β
β βββββββ¬βββββββ ββββββββββ¬ββββββββββ β
β β β β
β βββββββΌβββββββ ββββββββββΌββββββββββ β
β β Scheduler β βBackground Worker β β
β β(refetch) β β(expo-task-mgr) β β
β ββββββββββββββ ββββββββββββββββββββ β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β Sync Adapter Interface β
β read() | queue() | upload() | status() β
ββββββββββββββββββββ¬ βββββββββββββββββββββββ
β consumed by
ββββββββββββββββββββΌβββββββββββββββββββββββ
β SDUI Components β
β datasource | form | button | dataGrid β
βββββββββββββββββββββββββββββββββββββββββββ
Storage Strategyβ
| Data Type | Storage | Persistence |
|---|---|---|
| Cached model data | SQLite (per-record rows) with an in-memory mirror for synchronous reads, namespaced per organization | Survives app restart and re-authentication |
| Operation queue | Zustand store + AsyncStorage | Survives app restart |
| Attachment queue | Zustand store + file system | Survives app restart |
| Sync configuration | In-memory (parsed from YAML) | Re-parsed on module load |
| Network state | In-memory (NetInfo) | Real-time only |
The model cache deliberately does not use the app's TanStack Query persister: that cache is domain-scoped and reset on authentication changes, while sync data must survive re-login and be partitioned per organization. Cached models are stored in SQLite as one row per record, so writes stay proportional to what changed, there is no practical size ceiling, and a future incremental (delta) sync only needs row upserts. The engine hydrates the in-memory mirror at startup and re-delivers restored models to open datasources; a previously persisted AsyncStorage cache is migrated automatically on first launch.
Queue Processing Orderβ
When connectivity is restored, the background worker processes operations in this order:
- Attachment uploads (files persisted locally, identified by their client-generated GUIDs)
- Workflows with
dependsOn: [upload](execute once the attachments referenced in their own inputs are uploaded) - Independent workflows (no dependencies, processed in FIFO order)
- Model refresh (pull latest data after mutations complete)
Conflict Resolutionβ
The sync engine uses a last-write-wins strategy, with the server as the single source of truth:
- Cached model data is a read-only replica. The client never merges entity state locally β all writes go through workflow executions.
- Queued operations replay in their original order (subject to the processing order above) once connectivity is restored. Each replayed workflow runs full server-side validation and business logic, exactly as if it had been executed online.
- If the same entity was modified on the server while the device was offline, the replayed workflow's changes overwrite those fields β the engine does not detect or surface concurrent-edit conflicts. This is the "last write" in last-write-wins: the queued operation is the later write.
- If the server rejects a replayed workflow (entity deleted, invalid state transition, validation failure), the operation fails permanently per Failure Classification:
onSyncFailedfires,rollbackActionreverts any optimistic patch, and the operation lands in the Sync Operations screen for the user to resolve. - After the queue drains, all models refresh (step 4 above). The refreshed server state replaces the local cache, including any remaining optimistic patches β the cache always converges on the server.
Choose queue mode plus clear "saved offline" messaging when concurrent edits or server-side rejections are likely; last-write-wins is safest for field-worker flows where each entity is effectively owned by one user at a time.
Best Practicesβ
- Scope sync instances narrowly: Define per-module sync with only the data that module needs, rather than caching everything globally. This reduces storage usage and sync time.
- Use
dependsOnfor attachment workflows: Always declaredependsOn: [upload]for workflows that reference uploaded file IDs. This ensures the server never receives an attachment reference before the file itself has arrived. - Set appropriate
syncInterval: Frequently changing data (vehicles, orders) benefits from shorter intervals (300s). Reference data (locations, templates) can use longer intervals (3600s+). - Prefer
queuemode for data-creation workflows: Use optimistic mode only when you can reliably predict the server result. For complex workflows with server-side validation,queuemode with clear "saved offline" messaging is safer. - Configure
maxItemsfor large datasets: Prevent excessive storage usage by limiting cached items to what a field worker reasonably needs in a session. - Use
syncOn: [manual]for large reference data: Inspection templates or configuration data that rarely changes should only sync on demand to avoid unnecessary network traffic. - Handle
onSyncFailedgracefully: Provide clear user messaging and a path to the Sync Operations screen where they can retry or discard failed operations. - Test with airplane mode: Validate the complete offline flow by enabling airplane mode, performing all operations, then re-enabling connectivity to verify queue processing.
Related Topicsβ
- DataSource Component β Primary consumer of sync adapter for data loading
- Button Component β Workflow actions with
syncAdapterprop - Form Component β Form submissions with offline queue support
- Field Component (Attachment) β File upload integration with sync queue