FileMaker is, at its core, a multi-user database platform. Most serious solutions are hosted on FileMaker Server, accessed concurrently by anywhere from a handful to hundreds of users. And yet, multi-user behavior is one of the most commonly misunderstood aspects of FileMaker development. Developers build and test in a single-user environment, everything works perfectly, and then production reveals a class of problems that never appeared in development: record locking conflicts.
A record locking conflict occurs when one user (or process) holds a record open for editing and another user (or process) attempts to modify the same record. FileMaker's record-level locking model means that only one client can hold the write lock on a record at a time. When a second client tries to acquire that lock, it fails. What happens next depends entirely on how well the developer anticipated this scenario.
In the best case, the user gets a polite, informative message and can try again in a moment. In the worst case, a script silently fails to write data, commits partial changes, corrupts a transaction, or leaves a record locked indefinitely, with no indication to the user that anything went wrong.
This post covers FileMaker's record locking model in depth, explains why conflicts happen and why they're hard to see in development, walks through the failure modes that hurt production solutions, and provides the scripting patterns and design principles that make your solution resilient in a concurrent environment.
Understanding FileMaker's Record Locking Model
Before you can design around record locking, you need to understand exactly how it works.
Open vs. Committed Records
In FileMaker, a record exists in one of two states from the perspective of any given client:
- Committed: The record isn't being edited. Its data is stable. Any client can read it. No client holds a write lock on it.
- Open (in edit mode): A client has initiated editing. FileMaker has placed a write lock on this record for that client. No other client can open the same record for editing until the lock is released.
A record enters edit mode the moment any of the following occur:
- The user clicks into a field on a layout
- A script executes
Open Record/Request - A script executes
Set Field(which implicitly opens the record) - A script executes
Insertsteps that modify field data - A portal row is modified
The write lock is released when:
- The user commits the record (clicks outside the record, presses Enter, or the layout commits automatically)
- A script executes
Commit Records/Requests - A script executes
Revert Record/Request - The user's session disconnects
This model is called optimistic locking. FileMaker doesn't pre-reserve a record before a user begins editing. It allows editing to start freely and only enforces exclusivity at the write-lock level. The consequence is that conflicts are discovered at commit time, not at open time. By the time a conflict is detected, the user (or script) may have already done significant work.
The Error Codes You Need to Know
FileMaker surfaces record locking issues through Get(LastError). The codes relevant to multi-user conflict handling are:
- Error 200: Record is in use by another user.
- Error 301: Record is locked by another user (on
Open Record/Request). - Error 306: Record modification ID does not match (the record was modified since you opened it).
- Error 401: No records match this request. Not a locking error, but common in find-based workflows.
Error 301 is the "classic" locking error. You tried to open a record that another client already holds. Error 306 is subtler and more dangerous: it means that between the time you opened the record and the time you tried to commit it, another process modified it. FileMaker detected the mismatch via the record modification ID and refused the commit. This is the error that enables conflict detection even in workflows where two clients don't strictly collide on a lock.
Record Modification ID
Every record in FileMaker has an internal modification ID, a counter that increments each time the record is committed with changes. When you open a record, FileMaker records the current modification ID. When you commit, it checks whether the current modification ID still matches what it captured at open time. If another client committed a change to the same record in between, the IDs won't match, and FileMaker throws error 306.
This is an important safeguard, but it only works if your scripts check Get(LastError) after Commit Records/Requests. Many scripts don't. They commit and move on, assuming success. If the commit silently fails with error 306, partial data writes can result. Some fields were set, the commit failed, and the record reverted to its pre-edit state. The script continues as if the write succeeded. The user has no idea.
Why Record Locking Is Invisible in Development
The disconnect between development and production is the core reason record locking bugs are so prevalent.
Single-User Testing
Virtually every FileMaker developer builds and tests locally, with a single user session open. In this environment, there's no contention. No other client can be holding a record open. Every Open Record/Request succeeds. Every Commit Records/Requests succeeds. Scripts that don't check Get(LastError) appear to work correctly, because they do work correctly, in isolation.
The moment you put two or more users on the same hosted file, the assumptions break.
Low-Traffic Staging Environments
Even when developers test against a hosted development file, traffic is low and concurrent access is rare. The probability of two testers hitting the same record at exactly the same moment is close to zero. The conflict never manifests. The solution ships to production.
Fast Scripts Look Fine
A script that opens a record, sets fields, and commits in under a second looks safe. And in most cases, it is. But production introduces variability that development doesn't: network latency, slow client machines, users who walk away mid-edit, and high-throughput automated processes. The window of vulnerability is much larger in production than in any test environment.
Automated Processes Compound the Problem
FileMaker solutions often include server-side scheduled scripts, PSOS calls, and integration-triggered updates that run silently in the background. In development, these run in isolation. In production, they run while users are actively editing records, and they often target the same records users are working with. A nightly batch update that locks records for processing will conflict with users who start their day early. A webhook-triggered integration that updates a customer record will conflict with the sales rep who has that same customer record open.
The Failure Modes That Actually Hurt Solutions
Not all record locking failures are equal. Here's a look at what actually goes wrong in production, roughly ordered from least to most damaging.
The Dialog Bomb (Inconvenient but Recoverable)
The simplest failure: FileMaker presents its native record-locking dialog to the user. "This record is being modified by another user." The user clicks OK, waits, tries again.
This is recoverable, but it's a poor user experience. The native FileMaker dialog is abrupt, technical, and gives the user no guidance on what to do. In a well-designed solution, this dialog should never appear. It should be intercepted by proper error handling and replaced with a friendly, instructive message.
The Silent No-Op (Dangerous)
A script uses Set Error Capture [On] to suppress dialogs (correct behavior for scripts) but doesn't actually check Get(LastError) after operations that can fail. The script encounters a locked record, the error is suppressed, and the script either continues without writing the data or encounters a subsequent error that doesn't make sense in context.
The user sees nothing. No error, no warning. They believe their action succeeded. The data was never written.
This is particularly dangerous in workflows where correctness matters: financial transactions, inventory adjustments, status changes that trigger downstream processes.
The Partial Write (Very Dangerous)
A script opens a record, sets several fields, and then encounters an interruption: a locking error on a related record, a network hiccup, an unanticipated Get(LastError) result. The error handling path doesn't include a Revert Record/Request. The main record was partially modified before the failure. Some fields were set, others weren't. The commit that should have been atomic wasn't.
The record is now in an inconsistent state. Downstream calculations, reports, and processes that rely on that record will produce incorrect results. The inconsistency may persist indefinitely if there's no validation logic to catch it.
This is the FileMaker equivalent of a non-atomic database transaction, the exact class of corruption that transactions exist to prevent.
The Indefinite Lock (System-Degrading)
A script opens a record and then, due to a logic error or an unhandled error condition, terminates without committing or reverting. The record remains in edit mode. The write lock persists for the duration of that user's session, potentially hours.
Other scripts and users attempting to work with that record hit error 301 repeatedly. If the locked record is a high-traffic one (a settings record, a shared configuration, a central inventory item) the impact spreads across the entire user base.
The only recovery is the affected user closing and reopening FileMaker, or an administrator forcing the session closed on FileMaker Server. Neither is transparent to the business.
The Transaction Corruption (Worst Case)
The most severe failure involves a multi-record transaction: a workflow that modifies several related records as a unit, where either all changes should succeed or none should. Invoice creation is a classic example. Create the invoice header, create the line items, update inventory quantities, update customer balance. These are separate records. They need to succeed together.
If a locking conflict occurs midway through this transaction and error handling is absent or incomplete, you get partial writes across multiple records. The invoice exists but has no line items. Inventory was decremented but the line items weren't created. The customer balance reflects a transaction that didn't complete.
Recovering from this state may require manual data correction, forensic analysis of what happened, and potentially contacting affected users. It's the most expensive kind of FileMaker bug to fix after the fact.
The Transaction Pattern: The Foundation of Safe Multi-User Scripting
The single most important design pattern for multi-user FileMaker development is the transaction pattern. It provides atomicity, the guarantee that a set of related data modifications either all succeed or all fail cleanly, with no partial writes.
The Core Transaction Structure
Every script that modifies data should follow this structure:
# --- Setup ---
Set Error Capture [On]
Set Variable [$error; Value: 0]
Set Variable [$errorMessage; Value: ""]
# --- Open Record ---
Open Record/Request
Set Variable [$error; Value: Get(LastError)]
If [$error ≠ 0]
Set Variable [$errorMessage; Value:
"Could not open record for editing. It may be in use
by another user. (Error: " & $error & ")"]
Go to Script ["_HandleTransactionError";
Parameter: $$transactionContext]
Exit Script []
End If
# --- Modify Data ---
Set Field [TABLE::Field1; "value1"]
Set Variable [$error; Value: Get(LastError)]
If [$error ≠ 0]
Revert Record/Request [No dialog]
Set Variable [$errorMessage; Value:
"Could not set Field1. (Error: " & $error & ")"]
Go to Script ["_HandleTransactionError";
Parameter: $$transactionContext]
Exit Script []
End If
Set Field [TABLE::Field2; "value2"]
Set Variable [$error; Value: Get(LastError)]
If [$error ≠ 0]
Revert Record/Request [No dialog]
// ... handle error
Exit Script []
End If
# --- Commit ---
Commit Records/Requests [No dialog]
Set Variable [$error; Value: Get(LastError)]
If [$error ≠ 0]
Revert Record/Request [No dialog]
Set Variable [$errorMessage; Value:
"Could not save changes. The record may have been
modified by another user. Please try again.
(Error: " & $error & ")"]
Go to Script ["_HandleTransactionError";
Parameter: $$transactionContext]
Exit Script []
End If
# --- Success ---
// Continue with post-commit logic
The critical elements:
Set Error Capture [On]is always on. Native FileMaker dialogs never appear in scripted workflows.- Check
Get(LastError)after every fallible step. Not just commit, but open and set as well. - Revert before exiting on any error. Ensure the record is never left in an open/dirty state.
- Never continue after an unhandled error. Exit cleanly with appropriate messaging.
Centralizing Error Handling
The transaction pattern can be improved by centralizing error handling logic in a utility subscript rather than duplicating it across every script. A _HandleTransactionError script receives the error code, the error message, and any transaction context via parameter, logs the failure, presents the appropriate UI message, and ensures cleanup has occurred.
This means each calling script's error branches are reduced to: revert, set the error variable, and call the handler. The complexity lives in one place, and changes to error handling behavior propagate everywhere.
Multi-Record Transactions: Coordinating Across Records
When a transaction spans multiple records, the pattern extends:
# Phase 1: Open all records before modifying any
Open Record/Request [Record A]
If [Get(LastError) ≠ 0] → revert A, exit with error
Open Record/Request [Record B]
If [Get(LastError) ≠ 0] → revert A, revert B, exit with error
Open Record/Request [Record C]
If [Get(LastError) ≠ 0] → revert A, revert B, revert C, exit with error
# Phase 2: All records are open — now modify
Set Field [A::field; value]
Set Field [B::field; value]
Set Field [C::field; value]
# Phase 3: Commit all
Commit Records/Requests [Record A]
If [Get(LastError) ≠ 0] → revert A, B, C, exit with error
Commit Records/Requests [Record B]
If [Get(LastError) ≠ 0] → revert B, revert C (A already committed — log this), exit with error
Commit Records/Requests [Record C]
If [Get(LastError) ≠ 0] → revert C, exit with error (A and B committed — log)
Note the important caveat: once a record is committed, it can't be reverted. If Record A commits successfully and Record B fails, Record A's changes are permanent. This is a fundamental limitation of FileMaker's record-level transaction model. It doesn't have true database-level transactions (BEGIN TRANSACTION / ROLLBACK) that span multiple records.
The mitigation strategies are:
- Design your schema to minimize multi-record transactions. If a business operation can be captured in a single record (with related records as child rows), a single-record transaction covers everything.
- Order operations by reversibility. Commit the least-destructive changes last. If a failure occurs, the partial commit is easier to reconcile.
- Log every transaction step. Write an audit record for each commit within the transaction. If a failure occurs, the audit log gives you a forensic trail.
- Build compensating logic. For critical transactions where partial commits can't be tolerated, build a cleanup script that detects and reverses the partial state.
Retry Logic: Graceful Recovery from Transient Locks
Many record locking conflicts are transient. The record is locked for a fraction of a second by another script or user, and if you simply wait and retry, the conflict resolves. Building automatic retry logic is a significant improvement in user experience over simply reporting the error.
Basic Retry Pattern
Set Variable [$maxRetries; Value: 3]
Set Variable [$retryCount; Value: 0]
Set Variable [$success; Value: False]
Set Variable [$waitMilliseconds; Value: 500]
Loop
Exit Loop If [$retryCount ≥ $maxRetries]
Open Record/Request
Set Variable [$error; Value: Get(LastError)]
If [$error = 0]
Set Variable [$success; Value: True]
Exit Loop If [True]
Else If [$error = 301]
# Record locked by another user — wait and retry
Set Variable [$retryCount; Value: $retryCount + 1]
Pause/Resume Script [Duration (seconds): 0.5]
Else
# Non-locking error — don't retry, exit immediately
Exit Loop If [True]
End If
End Loop
If [Not $success]
// Handle permanent failure
End If
Exponential Backoff
For high-contention scenarios, linear retry (waiting the same interval each time) can cause "thundering herd" problems where multiple competing processes retry in lockstep. Exponential backoff, doubling the wait time each retry, spreads the retries out:
Set Variable [$waitSeconds; Value: 0.25 * (2 ^ $retryCount)]
Pause/Resume Script [Duration (seconds): $waitSeconds]
With three retries starting at 0.25 seconds: first retry at 0.25s, second at 0.5s, third at 1s. The total wait before giving up is under 2 seconds, imperceptible to a user, but effective at resolving most transient conflicts.
When Not to Retry
Retry logic is appropriate for error 301 (record locked by another user), a transient condition that will resolve when the other user releases the lock. It's not appropriate for:
- Error 306 (modification ID mismatch): This indicates a genuine data conflict, not a transient lock. Retrying may overwrite changes made by the other user. This requires conflict resolution logic, not retry.
- Non-locking errors (network failures, permission errors): Retrying these won't help and may mask the underlying problem.
Designing Layouts and UX to Minimize Conflicts
Record locking conflicts aren't purely a scripting problem. Layout design and UX choices significantly affect how long records stay open and how often conflicts occur.
Avoid Long-Running Open Records
The longer a record stays in edit mode, the greater the probability that another process will need it. I've found it helps to design your UX to minimize the time between a user opening a record and committing it:
- Don't use layout-based auto-open patterns where records enter edit mode as soon as the layout loads.
- Use card windows for editing. The record opens when the card opens, commits when it closes, and the main layout context remains committed.
- Implement explicit Save/Cancel button patterns. Don't rely on FileMaker's default commit-on-click-away behavior for complex forms.
Commit on Navigation
Configure layouts to commit records on navigation. If a user navigates away without committing, the record may remain locked depending on how FileMaker handles the context change. Explicit commits on all navigation paths eliminate this ambiguity.
Use Global Fields and Variables for Intermediate State
If a workflow requires accumulating user input over multiple steps before writing to the database, accumulate that state in global fields or script variables, not in live record fields. Write to the record only at the point of confirmed submission. This minimizes the time the record is open and reduces the number of commits per workflow.
Separate Lookup and Edit Contexts
A common source of long-lived locks is list views where records enter edit mode as users browse. A user scrolls through a list, accidentally clicks into a field, and holds the record open while they browse elsewhere. In practice, the best approach is to design list layouts as read-only with explicit "Edit" actions that open the record in a card window or detail layout. The list context is never in edit mode.
PSOS and Scheduled Scripts: The Hidden Locking Threat
Perform Script on Server (PSOS) and server-side scheduled scripts introduce a locking dynamic that catches many developers off guard: background processes competing with interactive users for the same records.
Why Server-Side Scripts Are Especially Dangerous
A server-side script runs without a UI, without user interaction, and often without the developer thinking about what records are open on the client side. It processes records sequentially, opens each one, modifies it, and commits. That's fast by human standards, but not instantaneous. During the window when a server-side script holds a record open, any client attempting to edit that record gets a locking conflict.
The scenarios that cause the most damage:
- Batch update scripts that iterate through thousands of records, holding each one open for a fraction of a second. Over many records, the probability that a specific user's current record is in that batch is non-trivial.
- Integration scripts triggered by webhooks or polling that update records as data arrives from external systems. These run at unpredictable times and affect records that users are actively editing.
- Scheduled maintenance scripts that run during business hours (because they were tested to be "fast") but lock records that are in active use.
Designing PSOS Scripts for Low Contention
- Run batch processes during off-hours whenever the business allows it. The simplest conflict avoidance is temporal separation.
- Minimize lock duration. Open the record, set fields, commit, move on. Don't do calculation-heavy work between open and commit. Do the heavy lifting before opening the record, store results in variables, then commit quickly.
- Use found sets, not loop-all. A PSOS script that loops through every record in a table to find the ones it needs locks each record briefly as it evaluates the condition. Use a find or
ExecuteSQLto build the found set before opening any records. - Check for conflicts explicitly. PSOS scripts should check
Get(LastError)onOpen Record/Requestand implement retry logic. A PSOS script that silently skips records on locking errors produces incomplete results with no visibility into what was skipped. - Log every failure. PSOS has no UI for reporting errors. If a locking conflict causes a record to be skipped, that fact should be written to a log table with the record ID, timestamp, and error code. Silent failures in background processes are the hardest class of bugs to diagnose.
Audit Logging: Your Forensic Trail
A recurring theme in multi-user conflict handling is that errors happen silently, and recovery requires knowing what was written before a failure. Audit logging is the infrastructure that makes post-failure forensics possible.
What to Log
For any operation that modifies critical records, log:
- Who made the change (
Get(AccountName)) - When it was made (
Get(CurrentTimestamp)) - What changed: old value and new value, or at minimum the new value and the operation type
- The transaction context: if this write was part of a multi-step transaction, what step was it?
- Success or failure: did the commit succeed?
- Error code if it failed
Logging Architecture
The standard approach in FileMaker is a dedicated Log or AuditLog table that receives one record per significant operation. The logging script is a utility subscript called from transaction scripts before and after commits.
Log records should be immutable. No delete privileges for standard user accounts, no edit access. The audit log is only useful as a forensic tool if it reflects what actually happened, not what someone wished had happened.
Using the Log for Recovery
When a transaction corruption is discovered (the invoice that has no line items, the inventory quantity that doesn't match the transaction history) the audit log is your starting point. It tells you what committed and what didn't, who was performing the operation, and at what time. With that information, manual data correction becomes a surgical procedure rather than a guessing game.
The Settings Record Problem
Many FileMaker solutions have one or more "settings" or "preferences" records: single-row tables that store global configuration values accessed by many scripts. These records are among the most commonly locked records in a solution, and locking conflicts on them can cascade to affect the entire user base.
Why Settings Records Are High-Risk
- They're accessed frequently, often on layout load, navigation, or at the start of scripted workflows.
- They're the same record for every user, maximizing contention.
- Scripts that modify settings records often do so as part of longer workflows, holding the lock for longer than necessary.
Design Patterns for Settings Records
Option 1: Read Rarely, Cache in Globals. Load settings into global fields at session open (OnFirstWindowOpen) and refresh them periodically, rather than reading the settings record on every access. Scripts work from the global cache. Only write back to the settings record when a setting actually changes. This dramatically reduces the number of times the settings record needs to be opened.
Option 2: Per-User Settings vs. Global Settings. Distinguish between settings that are truly global (apply to all users) and settings that are per-user preferences. Move per-user settings to a record in the Users or UserPreferences table keyed to the account name. Only global settings live in the shared settings record. Per-user records can be locked independently. Two users working with their own preference records never conflict.
Option 3: Minimize Write Operations. Audit every place in your solution that modifies the settings record. Many are unnecessary. A script that reads a counter from settings, increments it locally, and writes it back is a common antipattern. Replace incrementing counter patterns with Max(RelatedTable::ID) + 1 or UUID-based keys that require no shared state.
Version-Specific Considerations
FileMaker has evolved its handling of some locking scenarios across versions. A few key notes:
FileMaker 19+: The Data API and OData endpoints have their own locking behavior when making programmatic writes. API-initiated record modifications follow the same locking model as client modifications. An API call that attempts to modify a record held by a client session will fail with a locking error. API integrations need error handling and retry logic just as scripts do.
FileMaker 18+: PSOS supports a persistent session option for some use cases, which changes how global variables and session state behave in server-side contexts. If your PSOS scripts rely on session state for locking coordination, verify behavior against your specific version.
All versions: The fundamental locking model (optimistic, record-level, write-lock only) has been consistent throughout FileMaker's modern era. The scripting patterns described in this post apply across versions.
A Practical Checklist for Multi-User Safety
Use this checklist when reviewing any script that modifies data.
Error Handling
Set Error Capture [On]is the first step.Get(LastError)is checked afterOpen Record/Request.Get(LastError)is checked afterCommit Records/Requests.Get(LastError)is checked after anySet Fieldsteps in critical paths.- All error branches call
Revert Record/Request [No dialog]before exiting.
Transaction Integrity
- No code path exits the script with a record in open/dirty state.
- Multi-record transactions open all records before modifying any.
- Partial commit scenarios are documented and have compensating logic where required.
- Audit log entries are written for critical data modifications.
Retry Logic
- Scripts that may encounter transient locks implement retry with backoff.
- Retry logic distinguishes between retryable (error 301) and non-retryable errors.
- Maximum retry count and wait times are defined.
UX
- Users receive informative messages when a conflict is unresolvable.
- Messages include guidance on what to do (wait and retry, contact admin, etc.).
- Native FileMaker locking dialogs are suppressed and replaced.
PSOS / Server Scripts
- Server-side scripts check
Get(LastError)on all record operations. - Failures are logged to a log table with record ID, timestamp, and error code.
- Batch processes minimize lock duration (compute before open, commit quickly).
- Scheduling is reviewed for off-hours execution where possible.
Summary
Record locking conflicts are an inherent feature of multi-user database systems, not a FileMaker defect. FileMaker's optimistic, record-level locking model is well-suited to most business application patterns, but it requires developers to explicitly design for concurrent access. Solutions built with single-user assumptions will fail in production, often silently and in ways that are difficult to diagnose after the fact.
The foundation of multi-user safe FileMaker development rests on three things:
The transaction pattern: Every script that modifies data uses Set Error Capture [On], checks Get(LastError) at every fallible step, and reverts cleanly on any failure. No code path leaves a record open and dirty.
Retry logic with backoff: Transient locking conflicts (error 301) are handled gracefully with automatic retry, giving users a smooth experience when contention is brief.
Audit logging: Critical transactions are logged with enough detail to reconstruct what happened and recover from partial writes. Silent failures in background processes are never truly silent. They write to the log.
Beyond these foundations, schema design, layout design, and PSOS patterns all shape the frequency and severity of locking conflicts in production. A solution that minimizes open-record duration, separates read contexts from write contexts, and runs background processes outside business hours will see fewer conflicts than one that treats locking as a scripting afterthought.
Multi-user safety isn't a feature you add at the end. It's an architectural discipline you apply from the first script you write.