Race condition: Client-side edits overwriting server-side Bot outputs during background sync

Hi everyone,

I’ve run into a classic client-server sync edge case, and I’d love to hear how you all prefer to handle this in your apps.

The setup

I have a Grouped Action designed to generate a serial number (which is sequential) and an alias for a record. It consists of:

  1. Generate Serial No

  2. Use Serial No (update Serial No as Alias)

I specifically do not use Initial Value for this because calculating sequential serial numbers client-side introduces a massive risk of duplicate serial numbers due to concurrency. Relying on server-side execution is far better for mitigating duplicate sequential numbers (even though it isn’t 100% bulletproof).

Because of this, I trigger the action in two ways:

  • Bot trigger (Adds - Primary): An automation that runs on saving a form when a new record is created. This forces the sequential generation to happen on the server.

  • Manual trigger (Updates - Fallback): An action button on the record. This is strictly a fallback for edge cases to allow assigning a serial number manually if one is missing or if a duplicate somehow slipped through.

My current sync & feature configurations:

  • Performance: Sync on start: ON | Delayed sync: OFF | Automatic updates: ON

  • Features: Pull to refresh: ON | Preview new features: ON | New mobile framework (preview): ON | Desktop design: ON

The issue

When the fallback manual trigger is used, it works perfectly. It executes client-side, the local cache updates instantly, and the completed record syncs to the server.

However, when the Bot triggers on creation of a new record, I am hitting a race condition if the user edits the record while the background sync is still in progress.

Here’s the exact flow of the issue:

  1. The user creates a new record and submits the form (Serial No and Alias are initially blank).

  2. The server receives the row and triggers the Bot. The Bot successfully generates the sequential serial number and alias, updating the database. (I verified this by checking the “Automation Monitor” logs—the inputs and outputs are perfect!)

  3. The edge case: Before the background sync finishes pulling the Bot’s newly generated data down to the user’s device, the user makes a quick edit.

  4. Because the user’s local device hasn’t received the server update yet, their local cache still has a blank Serial No and Alias.

  5. The user saves their edit. The app pushes this locally cached row to the server, completely overwrites the server-side generated sequential Serial No and Alias in the database.

Observations on the sync UI

Interestingly, I noticed that if I disable “New mobile framework (preview)” and “Desktop design”, AppSheet occasionally provides a dedicated sync page UI that restricts user modifications during the sync. However, this blocking UI only appears for automatic 30-minute interval syncs or manual user-triggered syncs—not for record-level background syncs after a form save.

A feature request for the team

This highlights a great use case for a universal sync configuration. If enabled, would consistently lock the app state across all types of syncs (background form saves, automatic interval updates, and manual syncs). Having this seamless ‘lock state’ during any active sync would completely eliminate this race condition.

My Question

I am looking for some architectural workarounds to prevent this overwrite without compromising the server-side logic.

Would appreciate any insights or best practices!

Add an “updates” table that references the existing (“master” ) table. User-modified values get recorded in the updates table, along with a timestamp. The master table uses virtual columns to get the latest user-supplied value from the related updates rows. System-provided values, like serial number, get written directly to the master row. Added benefit: updates gives you the history of user changes.

Alternatively, go the other way: add an “adjuncts” table that references the master table, that contains the system-provided values. Use virtual columns in master to include the adjunct row values.

My only worry is scalability. If my app has, say, 30 tables and almost 20 of them need serial numbering, I’d have to spin up 20 extra tables just to handle this edge case. That feels way too expensive when it comes to maintaining the app long-term and would be a headache to manage.

Create a Serials table with columns Table (the table the serial is for), Row (the key column value of row of the table identified by [Table]), and Number (the actual serial number). Mark Table and Row as key columns, which will generate a virtual column named _ComputedKey to be the actual key. In the with items that need serial numbers, make the serial number column virtual of type Enum, base type Ref, and an App formula of (CONTEXT("Table") & ": " & [_THISROW]). When you give the item a serial number in Serial, it’ll show up automatically in the item.

Got it!

The trade-off here is data volume over structural complexity. Using one central Serials table instead of 20 adjunct ones means a higher row count, but it’s a clean, scalable way to completely bypass the race condition.

That said, I’d still love to see AppSheet introduce a native state lock configuration switch. It would handle this race condition out-of-the-box and resolve other architectural challenges like concurrency!