Using JS in FM

Persistent Data Store in FileMaker 2026

The Persistent Data Store in FileMaker 2026

A reference for JavaScript-in-FileMaker developers: what the feature is, what it replaces, and what the community has actually verified about it.

This page combines the official Claris documentation, published developer write-ups, and findings from a JS in FM Office Hours session where the feature was tested live. Where those sources disagree — and they do, on two significant points — the disagreement is documented rather than smoothed over.


A note on names

Claris calls it the persistent data store. You'll see three other names in the wild for the same feature:

  • "Persistent Data Storage" — DB Services' phrasing (DB Services)
  • "Draco Catalog" — Portage Bay's framing, after FileMaker's internal engine name (Portage Bay)
  • "Persistent Data Store catalog" — what it's called in the XML export (ScriptLogic)

The core idea: schema, not data

A set of named values saved as part of the schema in a FileMaker Pro file, not as record data. Unlike variables — which live in memory and are user-specific — entries remain available across sessions until explicitly deleted, and are accessible to all users of the file. — paraphrased from Claris Help: About the persistent data store

The framing that landed best in Office Hours was David Thorp's. Global fields and global variables are session-specific. Anything that isn't session-specific has, until now, had to live in a record you need a relationship to reach. The persistent data store has neither limitation — file-level globals without relationships.

That's the missing primitive. Everything else follows from it.

Soliant sharpens the same point: addressable from any script or calculation, in any context, without record navigation, without table occurrences, and without what they call globals' baggage (Soliant Consulting). LuminFire calls it <q>an incredibly massive architectural change disguised as a minor feature</q> (LuminFire).

Lee's observation in the session was that it simply feels more native — you reach it through a calculation, the same familiar mechanism you use for everything else in FileMaker. Which prompted Jeremy's note that "JavaScript is native FileMaker," an argument he took flak for years ago, is now closer to literally true: the code lives inside the FileMaker file, in the same catalog as themes and field definitions.

The patterns it replaces: settings tables, single-record config tables, global fields, global variables, custom functions returning hard-coded constants, dummy add-on tables, and external-files-as-config.


The API

Configure Persistent Data [ Name ; Instance ID ; Value ]Creates, updates, or deletes an entry
GetPersistentData ( name ; instanceID )Reads it back
ListPersistentDataIDs ( name )Lists the instance IDs registered under a name

Each entry is a triple: Name (required, can't be empty), Instance ID (optional text — a namespace or owner identifier), and Data. Name + Instance ID together are the unique key (Claris Help).

Supported everywhere: Pro, Go, WebDirect, Server, Cloud, Data API, and Custom Web Publishing (Claris Help: Configure Persistent Data).

Error handling:

  • Reading a nonexistent entry returns "?" — not empty. Claris' own example wraps it in a Let() that swaps in a default JSON object on "?" (Claris Help: GetPersistentData).
  • Deleting a nonexistent entry throws error 10, "Requested data is missing."
  • A blank Instance ID and an empty string "" are treated identically.

A naming complaint, raised independently by two people in the session: the step is called Configure Persistent Data, but everything analogous in FileMaker is Set — Set Field, Set Variable. Both Gabriel Woodger and Mark Johnson reported hunting through the script step list for "Set Persistent…" before finding it. Worth knowing if you're teaching this to someone.


The literal-name gotcha

The Name parameter is a literal, not an expression. Christian Schmitz tested this directly:

  • Configure Persistent Data [ $key ; ... ] creates an entry literally named $key. The variable is not resolved.
  • Configure Persistent Data [ "key" ; ... ] creates an entry named "key"with the quotation marks included in the name.
  • Only Configure Persistent Data [ key ; ... ] — bare, unquoted — produces an entry named key.

Instance ID is evaluated, so a variable works fine there. On the read side, GetPersistentData ( "key" ; $instanceID ) takes a normal quoted string expression.

So the write step and the read function use opposite conventions for the same value (Monkeybread Software).

Practical consequence: names must be hard-coded at design time. You cannot compute a name at runtime. All runtime namespacing has to happen through the Instance ID.


Data types survive the round trip

Claris documents that the store holds any FileMaker data type and returns the same type that went in. Schmitz verified it: numbers, dates, times, timestamps, text, and containers all round-trip intact, with — in his words — <q>No need to Base64 encode the containers!</q>

Gabriel noted in the session that container storage may let you exceed what a straight text field will hold, which makes template PDFs and logo images viable candidates. Mark LaRochelle has demonstrated exactly that: a company logo and identity details stored once in persistent data and accessed everywhere.

One wrinkle: the XML export always renders values as StyledText regardless of the real type. Store 123 as a number, read it back as a number, but the XML shows styled text.


Storing a JavaScript widget library

For a JS-in-FM solution this is the headline use case. Claris' documentation includes an example that injects a stored library straight into a web viewer's data:text/html URL:

Set Web Viewer [ Object Name: "webviewer" ; URL:
  "data:text/html," &
  "<html><head><script>" &
  GetPersistentData ( "ChartLibrary" ; "SharedLibraries" ) &
  "</script></head>" &
  "<body><div id='chart'></div></body></html>"
]

Claris frames the benefit as keeping large JavaScript out of calculation formulas and making it easier to update (Claris Help: GetPersistentData).

ISO FileMaker Magazine built a tutorial around exactly this pain — bundling a large library today means fighting character limits and duplicating the same code into places that were never meant to hold it (ISO FileMaker Magazine).

Size limit and workaround: you can't type a value into the Value option that exceeds the calculation formula limit. For anything larger, reference a field or variable — populated via Insert Text, Insert from URL, or Read from Data File (Claris Help).


Worked example: migrating a widget library out of a table

A ten-widget student information system, before and after.

Before

Widgets lived as records in an HTML table, keyed by a WidgetName field, with the markup in an HTML text field. Loading one from disk took an UploadToHTML script whose write path ran roughly fifteen steps:

New Window [ Style: Document ; Using layout: "HTML" (HTML) ]
Enter Find Mode [ Pause: Off ]
Set Field [ HTML::WidgetName ; "==" & $widgetName ]
Perform Find [ ]
If [ Get(FoundCount) = 0 ]
    New Record/Request
    Set Field [ HTML::WidgetName ; $widgetName ]
    Set Variable [ $message ; $widgetName & " has been added." ]
Else
    Set Variable [ $message ; $widgetName & " has been updated." ]
End If
Open Data File [ "$path" ; Target: $fileId ]
Read from Data File [ File ID: $fileId ; Target: HTML::HTML ; Read as: UTF-8 ]
Close Data File [ File ID: $fileId ]
Commit Records/Requests [ With dialog: On ]
Close Window [ Current Window ]

Only three steps read the file. The rest is ceremony to obtain record context — spawn a window to land on a layout tied to the right table occurrence, search for the record, create it if missing, commit, tear the window down.

The read side paid the same tax:

Substitute (
  HTML::HTML ;
  "<head>" ;
  "<head><script>window.fmClassId=" & Quote ( Classes::ClassID ) & ";</script>"
)

That HTML::HTML reference means the layout hosting the web viewer needs a table occurrence and a relationship path reaching the right widget record.

After

Open Data File [ "$path" ; Target: $fileId ]
Read from Data File [ File ID: $fileId ; Target: $html ; Read as: UTF-8 ]
Close Data File [ File ID: $fileId ]
Configure Persistent Data [ widgets ; Instance ID: $widgetName ; Value: $html ]

15 steps → 4. No window, no find, no record creation, no commit, no teardown. The find-or-create branch disappears because Configure Persistent Data creates-or-updates in one operation.

Mind the syntax: widgets must be typed bare and unquoted, but $widgetName in the Instance ID slot resolves correctly.

The read side becomes:

Substitute (
  GetPersistentData ( "widgets" ; "classRoster" ) ;
  "<head>" ;
  "<head><script>window.fmClassId=" & Quote ( Classes::ClassID ) & ";</script>"
)

What came off the relationship graph

This turned out to be the most concrete win. Supporting the old approach required an entire HTML table, ten records, and three extra table occurrences whose only job was to reach the right widget from the right context. On the courses layout, that meant a relationship from a field in Courses to a field in an HTML occurrence, sometimes set by script, purely to select which markup rendered.

All of it deleted. The only relational dependency remaining in the calculation is Classes::ClassID — which is genuinely data, and belongs there.

Changing which widget renders used to mean updating a field to change a relationship. Now it means passing a different string.

Naming: make the library enumerable

Claris' own example uses the Name for the setting key and the Instance ID for the add-on instance — e.g. com.claris.myaddon.theme with a UUID instance ID. Applied naively to a widget library, you'd make each widget a name and land in the enumeration trap described below: no ListPersistentDataNames means no way to ask what's in your library.

Inverting it — one name, widgets, with each widget as an instance ID — makes the whole collection enumerable through the one function that does exist:

ListPersistentDataIDs ( "widgets" )

classRoster, honorRoll, contactDirectory, staffDirectory, gradebook, attendanceCalendar, gpaTrendChart, enrollmentDashboard, studentReportCard, studentSchedule

Design principle: treat the Name as the collection and the Instance ID as the member. Under that convention the API is complete; under the other one it has a hole in it.

Two caveats: results come back in creation order rather than alphabetically, so wrap in SortValues if you're displaying them; and every widget shares one namespace, so a name collision silently overwrites.

The empty-state ?

Before anything is loaded:

ExpressionResult
GetPersistentData ( "widgets" ; "classRoster" )?

Not empty, not zero — a literal question mark. Any IsEmpty() guard written out of habit will sail straight past it.

As a teaching sequence: show the ?, run the loader, show the ten instance IDs, re-run the expression.


File size: smaller than the table approach

Measured across three versions of the same file:

VersionSize
Data only, no widgets979 KB
Widgets in an HTML table6.1 MB
Widgets in the persistent data store~5.1–5.3 MB

The persistent data store version is roughly 0.8 MB smaller — same data, same layouts, same ten widgets. The widgets themselves cost around 4–5 MB either way; moving them out of records recovers a little.

This is worth flagging because the expectation ran the other way. Clay Maeckel had reportedly indicated that catalog storage would make files larger, and Gabriel initially expected the same. The measured result contradicts it. Gabriel also observed that Save a Compacted Copy changes the numbers, suggesting the difference is down to how each store encodes the data rather than anything fundamental.

A real bloat risk does exist, just not here. Per a question Stephen Delantic raised in a Claris community session, space from deleted or replaced entries isn't reclaimed until the file is closed and reopened, or saved as a compacted copy — the same behaviour as deleting a large number of records. Irrelevant for a widget library you update occasionally. Very relevant if you're cycling megabytes of data through the store repeatedly.


When the data loads, and when it re-renders

These are two separate questions and the answers differ.

Loading. Gabriel's understanding, which he flagged as tested-but-inconclusive: when a client first connects, it downloads the whole catalog — layouts, scripts, tables, themes — and the persistent data store comes with it. Widget code stored in a field only transfers when you visit the layout holding the web viewer.

So the transfer moves rather than disappearing. You pay once at file open instead of on every layout visit. For a widget library that's a straight win. For a large store that most users never touch, it's a startup cost. Gabriel has asked Claris for detail on this and hasn't received a definitive answer.

Re-rendering — the important correction. The documentation says a change by one user is immediately visible to others, and the value genuinely does update immediately. But a web viewer bound to GetPersistentData does not re-render on its own.

Tested live in the session: a widget's code was updated, and the open web viewer showed nothing new. Resizing the window brought it in. So did toggling to layout mode and back, scrolling, and Refresh Window. In Gabriel's words, it doesn't push — but anything that causes the window to redraw makes it grab the new data. He suspects this is web viewer rendering behaviour rather than anything about persistent data itself, though a layout calculation referencing the store showed the same lag when tested.

Practical consequence: treat propagation as "next redraw," not "instant." For a notification widget that's fine — users navigating the system trigger redraws constantly. For anything needing guaranteed freshness, poll from inside the web viewer on a timer.

And a hazard Gabriel raised: the old field-based approach handed each user an isolated copy of the code at the moment they landed on the layout. Push a change now and a user mid-way through configuring something can have the widget re-render underneath them and lose their work. That safety came free before and doesn't now.


Security: no privilege model

The gap I flagged as unpublished has a clear answer from the session.

There is no user-level permission for persistent data. Anyone who can evaluate a calculation anywhere in the file can read every entry. Tables have privilege sets. Scripts have privilege sets. Persistent data has nothing — the closest analogue is a custom function, which likewise has no permission of its own.

On top of that, values appear as plain text in Save a Copy as XML. Three published sources independently reached the same warning:

  • Soliant: values are visible in XML output; treat it as configuration, not a vault.
  • MBS: not meant for secure data like OAuth keys — anyone could potentially read the values.
  • Codence: values appear as plain text in the export, so it isn't the place for secrets (Codence).

The session pushed back on the dogma before landing in the same place. Jeremy's counter-argument: custom functions are equally exposed, including through the DDR, and people store keys there anyway — so if you never export the XML, is it actually insecure? Gabriel's answer, which carried the room: someone eventually will export it without realising, or save it to a desktop that gets committed to GitHub, and over time the odds of none of those things ever happening are poor. Anything sensitive belongs in a table, protected by privilege sets, referenced by a full-access script.

A related consequence for anyone thinking about selling widgets: your code is fully visible in the XML export. Not a viable distribution model for anything you want to protect.


Older servers, and Perform Script on Server

A finding from the session that isn't in any published write-up, tested across two machines.

FileMaker Server 2024 hosting a file, accessed by two 2026 clients: persistent data works. Mark Johnson set an entry on one Mac and the second machine picked up the change. Because the store lives in the file rather than on the server, an older server can host it without understanding it — it reads and writes the bytes using existing catalog machinery, while the knowledge of what they mean lives in the 2026 client.

What breaks: anything running server-side. Perform Script on Server can't reach the store on a pre-2026 server, and neither can server-side scheduled scripts.

Workaround: read the value client-side, then pass it into Perform Script on Server as a script parameter.


Clone and Data Migration Tool: resolved

The published sources contradict each other on this, and the session settled it with a live test.

What Claris documents:

The persistent data store is not copied to the destination file when using the FileMaker data migration tool, because the persistent data store is not record data.

When you clone a file, persistent data store entries are included in the clone.

Claris Help

What several outlets claim: Portage Bay describes it as migration-safe, Winsoft lists DMT survival as a bullet, and MBS says values are included in migration.

What was actually tested: the session made a clone via Developer Utilities and checked. All ten entries survived. Gabriel had believed the opposite — that clones drop persistent data while migrations carry it — and corrected himself on the spot.

Why both sides were partly right. As Jeremy put it, a data migration in practice means: take the production data, make a clone of the current dev file, push the data into that clone, and promote it to production. The persistent data store rides along in the clone. It is never transferred by the DMT — Claris' documentation is precise and correct — but it survives the migration process, because the process is built on a clone. David Thorp's summary: it sits at the same level as schema.

The practical consequence. Entries authored in your dev file reach production. Entries written at runtime in production do not — the clone came from dev and knows nothing about them. So the use case Winsoft recommends it for, tracking whether a migration has already run, is precisely the one that breaks if the flag is written in production.


Enumerating names: the missing function

There is no ListPersistentDataNames. You can list the instance IDs for a given name, but there's no built-in way to ask which names exist in a file. David Head flags it as hopefully an oversight (ScriptLogic).

His workaround, in four steps:

  1. Export the catalog. FileMaker 26 adds a Persistent Data Store catalog to Save a Copy as XML, and the UI now allows saving each catalog to its own file. The script step accepts options as JSON, so you can request only PersistentStoreCatalog.
  2. Read it back with Open Data File / Read from Data File / Close Data File, leaving the byte count blank to read the whole document, as UTF-8.
  3. Parse with a While loop hunting for <PersistentStore name= and extracting what follows.
  4. Dedupe and sort with UniqueValues and SortValues.

The script requires Full Access.

Uses for the resulting list: diff it against a documented list to find orphaned entries, drive a scripted bulk delete, or check whether a name is in use.

Bonus finding from the same post: each entry's XML carries real audit metadata — a UUID, a modifications counter, userName, accountName, and a timestamp. Per-entry change tracking already exists; it's just only reachable through the XML export.

The broader complaint, raised in the session: the functionality shipped without an interface. There's no Manage Persistent Data dialog, no Data Viewer tab, no way to browse your buckets and click into one. Gabriel's read is that this looks like a small self-contained extension to existing catalog machinery — in the spirit of how the JSON functions arrived — with the UI plausibly coming later.


Using it for data, not just code

The session split on this, which makes it worth documenting as an open question rather than a recommendation.

Against. Mark Johnson's objection is concrete: the store is shared, file-wide. If you write a list of client IDs under widgets / studentReportCard and someone else runs the same script with different IDs, they overwrite you, and now you're both operating on the wrong records. There's no per-user isolation unless you deliberately namespace by Get(AccountName) in the Instance ID. Jeremy's position was similar — virtual list data is inherently single-use, and pushing it through a file-wide store is overkill.

For. Gabriel runs the opposite pattern in production, deliberately. A system with roughly 100 users maintains a single JSON array of outstanding tasks in the store. When anyone creates or completes a task, the array updates and every client's notification web viewer reflects it on their next redraw. Shared-by-default is the entire point: there's no other way in FileMaker to get a global pot of data that updates on everyone's screen when any one person changes it, without a relationship to a table. He reports it behaving flawlessly under load, and notes the efficiency argument — one push beats 100 clients polling a table asking whether anything changed.

Lee's framing captures it: it's a data broadcast.

The reconciliation: shared-and-overwritable is a bug when you wanted per-session state and a feature when you wanted a broadcast. The trap is reaching for it as a global variable replacement without noticing it isn't per-user.

Mark LaRochelle has separately suggested using it to work around the million-character limit on Execute Data API results — push the found set into persistent data instead of returning it. Gabriel noted the established alternative: have the server write the payload to a record, return the key, let the client fetch and delete it.


Distribution and multi-file patterns

Several ideas surfaced that nobody has built yet:

  • A generic getter in a shared file. Mark Johnson's suggestion: one script in a utility file that takes a widget name and returns the code, called from anywhere.
  • Multi-tenant sync. Gabriel's: if you manage many deployments, you could script widget updates into each file's store without a full migration.
  • Versioned remote updates. Joseph Ricciardi's: host widget code externally, have files check a version number on open, pull down and refresh their own store. Effectively self-updating widgets with no client deployment.

The obvious blocker on anything commercial is the XML visibility described above.

Speculation worth recording: Jeremy's read is that the persistent data store exists because of what's coming — that Claris' agentic tooling will need somewhere to put generated web viewer code, and this is that somewhere. Gabriel sketched the two competing directions: AI that writes native FileMaker scripts and layouts, versus FileMaker as a backend with HTML/JS front ends. Persistent data is a natural home for the second.


Attribution

The Office Hours findings above come from a JS in FM community session with contributions from Gabriel Woodger, Mark Johnson, David Thorp, Joseph Ricciardi, and Lee. Points attributed to Matt Petrowski, Mark LaRochelle, Clay Maeckel, and Stephen Delantic were relayed second-hand in that discussion and are marked as such.

Sources

SourceContribution
Claris Help — About the persistent data storeAuthoritative definition, clone/DMT behaviour
Claris Help — Configure Persistent DataOptions, compatibility, size workaround
Claris Help — GetPersistentData"?" return, web viewer example
Claris Release NotesOfficial use cases, new privileges
ScriptLogic (David Head)Missing names function, XML catalog workaround, audit metadata
Monkeybread Software (Christian Schmitz)Literal-name gotcha, type testing, transactions, server logs
Soliant ConsultingArchitectural framing, security warning
ISO FileMaker MagazineJS library tutorial
CodenceAI prompt storage, security
LuminFireFraming
Portage Bay"Draco Catalog" naming, DMT claim
WinsoftUse-case list, DMT claim
DB Services"Persistent Data Storage" naming

Two published errors worth not repeating: DB Services and Neptune Digital both write the reader as Get(PersistentData). It's GetPersistentData() — a Miscellaneous function, not a Get function.

Previous
Introduction