SaXML4ADT
Save-as-XML for Agentic Development Toolkit. A command-line query store over a
FileMaker Save a Copy as XML export: exact cross-references, readable scripts,
impact analysis, a freshness model, and a web console — built so an agent doing
FileMaker development with fm-cli can answer
"what will this touch?" in milliseconds, without taking FileMaker's schema lock,
and can tell when its answer is out of date.
Read-only. fm-cli stays the only writer. See docs/USE-CASES.md
for the use cases this was built against and the FileMaker quirks found on the way.
The idea of a queryable store over FileMaker's own structural export came from Nuosis/FM2WEB_CLI (a DDR toolkit); SaXML4ADT is a from-scratch implementation around Save-as-XML and shares no code with it.
Prerequisites
| Python 3.10+ | pipx recommended |
| An ADT project | a folder with adt.json naming the file — the ADT toolkit's own adt init, not this tool's saxml4adt init (see Install below) — and fm-cli on PATH with its keychain credentials for the file (fm --file=… --username=… --store-credentials) |
| FileMaker Server with the Data API on | and an account with the fmrest extended privilege (the same account adt.json names is fine). Not hosted? See Working without a server below |
| Any coding agent | Claude Code gets the freshness hook (saxml4adt init); agents without hooks (Codex, …) run fm-cli through saxml4adt fm …, which marks for them — saxml4adt init --agents writes that protocol into AGENTS.md |
Install
pipx install saxml4adt # or: pip install saxml4adt
cd ~/projects/my-adt-project # the folder with adt.json (or an empty folder)
saxml4adt serve # opens the browser on the Setup screen
The bleeding edge, ahead of the last PyPI release: pipx install git+https://github.com/mw777eds/SaXML4ADT.
The Setup screen does the rest. It shapes itself around what the project already has: with no
file recorded it shows the adt connect command that records one, because that is ADT's file to
write; with a hosted file it stores the Data API password and nothing else, the account coming
from adt.json; with a local file there is no password to store and it says so. Then readiness
checks, and buttons for the steps that can actually run — the console appears when the first
build lands. The same steps from the terminal:
saxml4adt init # hook + .gitignore + readiness check
saxml4adt install # once per FileMaker file (see below)
saxml4adt export --save # first time: asks for the password, stores it
saxml4adt build
saxml4adt serve
adt.json is ADT's file, and ADT writes it: adt init --target fmnet://fms.example.com/MyFile
in an empty folder, or adt connect <target> in a project that already has one, where <target>
is an fmnet address or the path to a .fmp12. SaXML4ADT reads it and never writes it, so there is
one record of which file a project means. A FileMaker account with Full Access is required —
record it with adt connect --username <account>.
init writes the hook into .claude/settings.json, adds the export folder and
the store to .gitignore (they are client data), and reports what is missing.
install uses fm-cli to add one table (SaXML4ADT_Export), one layout of the
same name, and three scripts in a SaXML4ADT folder — the machinery that runs
Save a Copy as XML on the server and hands the files back through the Data API.
Everything it adds is in saxml4adt/ops/install.ndjson
for review. Put the server's full host name in adt.json
(fmnet://fms.example.com/File) so the Data API certificate matches; a short
alias can be mapped in ~/.config/saxml4adt/hosts.json ({"fms": "fms.example.com"}).
Credentials go in the OS credential store (macOS Keychain as a saxml4adt: host/file
item; keyring elsewhere), or in SAXML4ADT_FM_PASSWORD for CI.
saxml4adt credentials shows whether one is stored; --forget removes it and --username NEW --save
stores the next one. To change the account fm-cli edits as, update username in adt.json
(or run adt connect) and fm --forget-credentials / --store-credentials its own entry —
SaXML4ADT never reads or writes FileMaker's keychain items.
Renamed from saxml. Earlier builds installed a saxml command; it is gone. An agent whose
saxml … calls start failing with command not found mid-session is seeing the rename, not a
broken install — switch to saxml4adt (same subcommands) and re-run saxml4adt init so the
hook and .gitignore entries carry the new name.
Help
saxml4adt help lists topics, saxml4adt help scripts the commands in one, saxml4adt help steps one
command with its options and examples (--json for agents). saxml4adt <command> --help still works.
saxml4adt help concepts lists the mental models behind the store (marked vs unmarked, freshness,
the hook loop, …); saxml4adt help concepts marked-vs-unmarked prints one.
The loop
saxml4adt export → saxml4adt build → query / serve
▲ │
└── agent edits with fm → hook marks dirty ─┘
- export runs the server-side Save a Copy as XML and pulls every catalog file
into
XML/SaveAsXML/<File>/(--catalogs ScriptCatalog,LayoutCatalogfor a partial export, merged into the folder).--transport sshrsyncs the server's Documents folder instead, for hosts you can reach.pulldownloads an export the server already produced (afterexport --keep) without running it again. Anotein the output when fewer files loaded than expected usually meansSummary.xml— every structural catalog still loaded; it's not FileMaker skipping an empty one, and SaXML4ADT never readsSummary.xmlanyway. - build loads the export into
saxml4adt.sqlitein place: the build history, the change log and the dirty marks survive, and every object that changed since the previous build is reconciled against the marks (--freshwipes). Seconds, even for a 70 MB export. - serve opens the console: a map of catalogs, which objects are marked
dirty / changed / changed-without-a-mark, drill-down to any object, the op
that caused each mark, history by account, and the query log. It follows the
store live. The server runs in the background — the terminal is free.
Only one tab opens per project: running
serveagain while it's already up prints the existing URL and does not start a second server or open another tab;serve --restartstops and restarts it in place (same port) without opening a tab either — the console page you already have open detects the restart and reconnects on its own.serve --stop(or the title menu, which also closes that tab) ends it, and it quits by itself after 12 hours without a visitor. - Every query prints JSON with
meta.stale— the open marks the result touches — and warns;--strictexits 3 on a stale answer,--max-age 30mexits 4 on an old export, andstatusreports catalogs whose source files are gone or newer than the build.
What it answers that fm cannot
saxml4adt steps <script> |
every step with 0-based index and 1-based line, options decoded by name — including the 51 step types fm exposes only as numbered slots (the XML's position="N" equals fm's slot index), and calcs fm renders as <Function Missing> |
saxml4adt function <name> |
custom function bodies (fm cannot read them) |
saxml4adt broken-refs |
dangling Field/Script/Layout/TO references, including ones on layout objects that FileMaker's own problems list omits |
saxml4adt themes / styles <theme> |
every theme in the file and its named styles — the only names create:layout / addObjects will accept |
saxml4adt duplicates script |
name collisions fm cannot address individually |
saxml4adt taborder <layout> |
the tab order as FileMaker stores it — per-object positions, gaps left by deleted objects, and objects that have no position at all (everything fm adds), which is why fm-added fields land unpredictably; fm has no tab-order surface |
What it answers faster
usages Table::field · refs-to layout X · callers / callees / triggers ·
describe layout|script|to|field · objects <layout> (with parent ids and
enclosing path) · variables --problems / variable $$name (assignments vs reads, case-insensitive like FileMaker, spelling drift) · search (FTS5 over
steps, calcs, comments, and object names, ranked ahead of content hits) · path TO-A TO-B · relations --suspicious ·
steps-of-type "Send Mail" --where "No dialog=Off" · fields-on <layout> ·
portals · unreferenced (never "unused" — read its caveat).
Freshness: an export is a snapshot
saxml4adt describe layout Settings # read
fm --file=… ops.ndjson # write with fm-cli
saxml4adt invalidate --from-ops ops.ndjson --as agent-2 # mark what the batch touched
saxml4adt describe layout Settings # -> meta.stale warns; --strict exits 3
saxml4adt refresh ~/exports/MyFile/SaveAsXML --catalogs LayoutCatalog # re-ingest only that catalog
invalidate <kind> <name>marks one object;--from-opsmarks every write target in an fm-cli batch;invalidate catalog ScriptCatalogmarks a whole catalog.ops.ndjson(also whatimpactpreflights, below) is fm-cli's own batch format — one write per line, e.g.{"op":"create:field","table":"PRF__Preferences","name":"smtpAddress","type":"text"}.--max-age 2hrefuses to answer from an export older than that (exit 4).refresh --catalogs A,Breplaces only those catalogs from a new (possibly partial) export and clears their marks. FileMaker can produce a partial export: the Save a Copy as XML script step takes options JSON{"catalogs_included":["ScriptCatalog"],"split_catalogs":true}.hooks/saxml4adt-invalidate-after-fm.shis a Claude CodePostToolUsehook that runsinvalidate --from-opsautomatically after anyfm …command, so the discipline does not depend on the agent remembering.- Hook discipline that is still on the agent: write ops to a file (ops on
stdin/heredoc are invisible to the hook, which then marks every writable
catalog dirty), and export
SAXML4ADT_ACTORso marks andtouched --bycan tell agents apart. The hook skips--dry-runand batches fm rolled back. - Every object row records
modified_by,modified_atand itsmodificationscounter from the XML, so with an agent-specific FileMaker account a re-export also shows what that account touched.
History and drift
Every object row carries the XML's own modifications counter and
modified_by/modified_at. When a later export is ingested (build or
refresh), SaXML4ADT diffs those against the previous state and records what the
export showed changed — then reconciles it against what agents said changed:
saxml4adt refresh XML/SaveAsXML --catalogs ScriptCatalog # prints drift: changes / unmarked / marked_unchanged
saxml4adt drift # last refresh: predicted, changed-but-unmarked, marked-but-unchanged
saxml4adt history --by agent-bot --since 2026-08-28 # observed changes over time, with who and whether it was marked
saxml4adt touched --by agent-bot # straight from the export's stamps, no history needed
saxml4adt builds
unmarked_change is the alarm this whole model exists for: three agents and a
person share one file, and the failure that actually happens is a peer writing to
a file you own without your knowing. unmarked changes mean someone edited without telling the store (an agent that
skipped invalidate, or a person in FileMaker Pro). marked_unchanged means a
mark was a false alarm or the write never landed. Give each agent its own
FileMaker account and the export becomes its audit trail.
Every version, without keeping the XML
Each build also records what changed inside every object — attribute before/after, script
steps as line hunks — in change_log. Nothing else is kept, so the store stays small, yet any
version since the store's second build can be reconstructed by replaying the deltas backwards:
saxml4adt history --kind script --object "Nightly Sync" # every build that changed it, with the deltas
saxml4adt show script "Nightly Sync" --at 3 # the script as build 3's export showed it
saxml4adt diff script "Nightly Sync" --from 3 --to 5 # attribute before/after + unified diff of steps
saxml4adt diff field "Contacts::email" --from 2 # …to current
Works for every kind (layout, layout_object, field, table_occurrence, value_list,
custom_function, …); the console's object panel shows the same timeline.
Web-viewer persistent stores (ADT [<app>] and friends, under PersistentStoreCatalog) get the
same treatment: each build records the payload's sha1, size, modification count, account and
timestamp — never the payload — so a redeploy shows up as an ordinary change_log row (old/new
sha1 + size) via history/show/diff, same as any other object.
saxml4adt status also compares each ADT [<app>] entry's recorded sha1 against
webviewer-apps/<app>/dist/index.html on disk and reports it under a webviewer block, so a stale
deploy is visible without opening FileMaker.
Web console
saxml4adt serve # background server + browser; `serve --stop` ends it, 12 h idle timeout
saxml4adt serve --restart # stop + start in place, same port, no new tab (the open tab reconnects)
saxml4adt serve --foreground --port 8770 # in this terminal instead (Ctrl-C stops it)
saxml4adt export-web console.html # self-contained snapshot with the data embedded
Home is a review feed: "since your last visit" — the builds you have not seen, then one row per changed object with a pill (✓ marked / ⚠ unmarked / a twin-export notice), the kind and name, one line of what changed (step hunks, moved objects, the attributes), and who touched it when. The name opens the object; the what-changed line opens it on the panel that shows that change. Mark all reviewed advances your last-seen build (kept per store in this browser), and show all builds → is History. The catalog map moved one click away, to the Catalogs tab and the rail.
From there: drill-down to objects with their marks, observed changes, steps, layout objects and references; a history view with the per-refresh drift bars and who-touched-what; the query log. Marks can be made from the page when served live.
Every interaction is one of three verbs, and no click loses your place: select answers in place (clicking an object in a layout preview outlines it and fills an inspector directly below — bounds, enclosure breadcrumb, theme style and local overrides, field binding, last change; ⌥-click adds more; in a script, click a step and shift-click another for a range), peek opens over the view and Esc restores it exactly, and navigate is the only thing that reroutes. Every list that names an object — references, where-used, a layout's objects, a variable's scripts — peeks first: a slide-over brief with the facts, its last change and its reference counts, and one Open → when you actually want to go there.
An investigation trail runs under the header: every deliberate navigation leaves a chip (kind + name), and clicking one returns to that stop with its scroll position intact — an agent's stop lands there too, in amber. Back means the previous chip; Home starts a fresh trail. It is session-local and sits above the browser's own Back, which keeps working.
The pane boundaries are drag handles (rail | list | detail); widths persist per browser
and a double-click resets one. ☰ collapses the catalog rail to a one-column icon strip.
docs/CONSOLE-IA.md is the full contract, including every hash parameter a deep link can
carry.
Pins and agent navigation
📌 Pin on any selection adds an entry to your pins — layout objects, a range of
script steps, a calc fragment — instead of replacing the last one. The header button
shows a live count and peeks the list. Say "look at my pins" and the agent reads all of
them in one pull with the pinned_items MCP tool; reading grays them into a short
history rather than deleting them, so they stay re-askable until you clear them. Nothing
is ever pasted into your prompt.
The other direction: an agent can put something on your screen with console_show,
which drives the open tab to a view and shows a toast saying what it is showing and why.
Your previous view and scroll are saved first, and Esc — or the ↩ Esc returns you
chip that outlives the toast — puts you back.
MCP server
saxml4adt mcp speaks the Model Context Protocol over stdio
(JSON-RPC 2.0, newline-delimited), so Claude Desktop, Claude Code, Cursor and any other MCP client
can query the store directly — without shelling out to saxml4adt. It is hand-rolled (stdlib +
the rest of this package only; no mcp package dependency) and read-only except for one tool,
mark_dirty, which writes to the store's own freshness ledger (never to the FileMaker file —
fm-cli stays the only writer of that).
saxml4adt mcp --print-config # Claude Desktop snippet + the `claude mcp add` line, with an absolute --db path
saxml4adt mcp --db ~/projects/my-adt-project/saxml4adt.sqlite # run it (a client normally launches this itself)
--print-config fills in an absolute path so the config works regardless of the client's working
directory:
claude mcp add saxml4adt -- saxml4adt mcp --db /Users/you/projects/my-adt-project/saxml4adt.sqlite
or, in claude_desktop_config.json:
{"mcpServers": {"saxml4adt": {"command": "saxml4adt", "args": ["mcp", "--db", "/Users/you/projects/my-adt-project/saxml4adt.sqlite"]}}}
Tools — one per read command that matters for an agent mid-task, each taking the same option
names as its CLI counterpart and returning the same {meta, …} envelope (so meta.stale still
warns when a result touches a mark made since the export): overview, status, files, search,
object (= describe), referenced_by (= refs-to, impact analysis), script (= steps),
layout_objects (= objects), layout_html, styling, style_suggestions, variables,
history, show_at (= show), diff, blame, perf, conventions_check (= conventions),
unreferenced, query_log, and mark_dirty (= invalidate, the one write tool). Each result
comes back as content: [{type: "text", text: <json>}] plus a matching structuredContent.
Two more talk to the console rather than the export:
pinned_items— the items the user pinned in the console for you to look at, each with a ready-to-read text block and the structured data behind it. Reading marks them read (they stay in the console, grayed);{"include_read": true}brings them back.console_show(to, note)— put something on the user's screen: drives their open console tab to a view, announces itself in a toast, and leaves Esc pointing back at where they were. Reports whether a tab actually applied it, and returns the full URL for when none is open. Needs the console to be running (saxml4adt serve) and to be launched from the project folder.
Resources: saxml4adt://overview (the same JSON as the overview tool) and
saxml4adt://layout/<name>.html — one per layout, the same rendered standalone-HTML preview
layout-html writes to disk, with dummy sample data.
The server connects to the store lazily, so initialize / tools/list still answer before a
build exists; a tool call against a missing store comes back as that one call's error, not a dead
connection. Nothing but JSON-RPC ever reaches stdout — diagnostics go to stderr.
Layout snapshots (macOS)
saxml4adt snapshot captures real FileMaker Pro layouts as PNGs — ground truth to compare
against layout-html's renders. Once per run it raises the file's document window to the front
of FileMaker's own window list (see the requirements below — this step matters), then switches
the live layout with AppleScript, captures the document window with Quartz + screencapture
(works on an occluded window, steals no focus), and records the layout FileMaker actually loaded,
since an OnLayoutLoad trigger can redirect.
saxml4adt snapshot "Contact Detail" # one layout -> ./Snapshots/Contact Detail.png
saxml4adt snapshot --all --out Snapshots/Empowered_Beginning --compare # every layout + a side-by-side compare.html
saxml4adt snapshot "Contact Detail" "Invoice Detail" --file Empowered_Contacts --delay 2
Requirements:
- macOS, with FileMaker Pro running and the target file already open (not just hosted — open in this copy of Pro).
- Screen Recording permission for whichever app runs
saxml4adt(Terminal, iTerm, etc.) — System Settings → Privacy & Security → Screen Recording. - The
fmextscriptaccessextended privilege on the account signed in to the file, for AppleScript layout control. Granting it does not take effect on an already-open file — close and reopen it first. go to layoutandget name of current layout of database "X"both act on that database's FRONT window — not necessarily the document window a user would recognize. A hidden card window (this package's own "ADT MCP Server Connector" window is a real example) can be frontmost instead; when that happens, every switch and readback silently happens there while the visible document window never moves, so every captured PNG comes back byte-identical regardless of which layout was requested (current layouteven reads back as matching, since it is honestly reporting the connector window's layout — the mismatch only shows up as pixels that never change).snapshothandles this itself: before the sweep it raises the document window it found via Quartz to the front of FileMaker's own window list (go to window, or a System EventsAXRaisefallback if that verb isn't in FileMaker's dictionary — the fallback needs Accessibility automation permission granted to the host running this command), so every later switch/readback in the run actually targets it. If you ever see a run come back with every PNG hashing identically, this raise is what to check first.- A modal FileMaker dialog (a
Show Custom Dialogstep, the "Summarize" field dialog, …) blocking a layout switch is also handled automatically — it is detected, the app is activated and Escape is sent, and the switch is retried once before that one layout is given up on and the sweep moves on (seewarning/dialog_layoutsin the JSON summary). One bad layout — a dialog that doesn't clear, a timed-out AppleEvent, anything else — is always isolated to its own row; it never aborts the rest of the sweep.
--compare also renders each captured layout with layout-html --sample dummy into the output
directory and writes <out>/compare.html: the FileMaker PNG and the HTML render side by side per
layout, for eyeballing fidelity. It is a plain local file — nothing gets published anywhere.
The JSON summary reports, per layout, {layout, actual, png, width, height, warning?}, plus a
top-level dialog_layouts list of any layouts that hit a modal dialog. actual and warning
only differ from the requested name when an OnLayoutLoad trigger redirected the view; --all
skips layout names starting with . and keeps going past a failed switch, so one bad layout does
not abort the run.
Working without a server
No FileMaker Server, no Data API, no credentials — a file open locally in FileMaker Pro (FM Starting Point, or any solution you have open on your own machine) is enough, and the console runs against the resulting store exactly the way it does for a hosted project.
There are two ways to produce the export. By hand, with Save a Copy as XML in Pro, which
needs nothing installed at all. Or with saxml4adt install, which puts an export script in
the file through fm-cli — fm-cli opens a local .fmp12 by path, so this works with no
server and, for an unprotected file, no account. That script detects that the file is not
hosted and writes the export straight into XML/SaveAsXML/<File>/ beside the file, which
is where build already looks. Both routes end in the same place; the script is worth the
one-time install if you will re-export more than a couple of times.
pipx install saxml4adt
mkdir my-project && cd my-project
adt init --target ./FM_Starting_Point.fmp12 --username <account> # ADT records the file
saxml4adt init # hook, .gitignore, MCP, skill
Keep the .fmp12 in the project folder: the export script writes beside the file, which is then
also where build looks. Nothing in this path calls the Data API — the account exists so fm-cli
can open the file, not to log in to a server. saxml4adt init installs the PostToolUse hook, adds
saxml4adt.sqlite*, .saxml4adt/ and XML/SaveAsXML/ to .gitignore, registers the MCP server
in .mcp.json and writes the agent skill; saxml4adt build will find an export
under XML/SaveAsXML/<File>/ on its own even without ever running init, so this step is
convenience, not a requirement.
saxml4adt serve's Setup screen (the one under Install above) has a first-class local mode:
it names the file the project is pointed at, says there is no Data API and no password to store,
and offers Build from files on disk as the step that applies here.
With the file open in Pro — locally, not just hosted-and-connected — run File → Save a Copy as XML…. Three of its options matter here (confirm the exact wording against your copy of Pro; these are the option names, not necessarily the on-screen labels):
- split the catalogs into one file per catalog rather than one combined XML file —
saxml4adt buildreads either shape, but everything below assumes the split one - include the analysis / Database Design Report details — this is what makes tab order
(
saxml4adt taborder) available at all; skip it and every layout's object order goes unrecorded, not merely incomplete - keep binary data in the export — the switch that decides whether button icons and layout images are written out at all; turn it off and every layout preview in the console renders without its images and icons, though everything else about the object is still tracked normally
Save into XML/SaveAsXML/<File>/ next to the project folder — <File> is the live file's
name without .fmp12, which is what FileMaker names the split files after, and has to
match the file ADT recorded. Then:
saxml4adt build
saxml4adt serve
Repeating the export and saxml4adt build later reconciles the store against whatever
changed since — no watcher required, though saxml4adt watch (poll a folder for a fresher
export) works here too.
What this path cannot do: export, pull and credentials reach a server — the first two
fetch an export the hosted file cannot hand over any other way, and the third stores a Data
API password there is nothing here to log in to. Everything backed by fm-cli rather than the
Data API does work: install, uninstall, verify-layouts and saxml4adt fm all open a
local file by path. Nearly everything else behaves exactly as it does for a hosted
project: every query command, refresh --catalogs (against a fresh manual export instead
of a pulled one), mcp, watch, export-web, and snapshot (which drives FileMaker Pro
directly over AppleScript and never touches the Data API, hosted or not).
saxml4adt doctor (and init's own readiness check) judges a project against what its own
target actually needs, so a healthy local project reports "ok": true and exits 0. The
checks that belong to the hosted path are listed separately under not_applicable, each
with the reason it was skipped — a stored password and the Data API, for a local file. A
requirement that does not apply is not a requirement you have failed to meet, and the
distinction matters most here, where half the checklist is about a server you do not have.
Preflight an fm-cli batch
saxml4adt impact ops.ndjson
For each op: does the target exist in the export, is its name ambiguous, what
references would a delete break, and do the table / occurrence / theme names it
mentions resolve — all before fm opens the file and takes the schema lock.
Verify a write
saxml4adt build before/ --db a.sqlite && saxml4adt build after/ --db b.sqlite
saxml4adt diff-stores a.sqlite b.sqlite --script "Do Thing" --assert-only-changed 118 # exit 6 if anything else moved
Conventions
saxml4adt conventions checks every table, table occurrence, field, script, variable and custom
function name against a naming-conventions file — saxml4adt.conventions.json, next to
saxml4adt.sqlite in the project folder:
{
"version": 1,
"tokens": { "code": "[A-Z]{3}", "Code": "[A-Z][a-z]{2}", "lcode": "[a-z]{3}", "Name": "[A-Z][A-Za-z0-9]*", "name": "[a-z][A-Za-z0-9]*", "NAME": "[A-Z][A-Z0-9_]*" },
"rules": {
"table": { "pattern": "{code}__{Name}", "example": "CNT__Contacts" },
"table_occurrence": { "base": "{code}__{Name}", "related": "{lcode}_{code}__{Name}", "example": "cnt_ADR__Addresses" },
"field": { "pattern": "{name}", "example": "firstName" },
"field_global": { "pattern": "{name}_g", "example": "sessionId_g" },
"field_key": { "pattern": "_{name}", "example": "_contactId" },
"script": { "pattern": "{Name}", "example": "Contact_Create" },
"variable": { "pattern": "${name}", "example": "$contactId" },
"variable_global": { "pattern": "$${NAME}", "example": "$$CURRENT_USER" },
"custom_function": { "pattern": "{name}", "example": "trimAll" }
},
"ignore": ["^zz_", "^Global$"]
}
Each {token} in a pattern substitutes the named regex from tokens; every other character is
literal (a $ in variable/variable_global matches a literal $). A rule may give several
named alternatives instead of one pattern — table_occurrence above passes a name that matches
either base (a table occurrence standing in for its own base table) or related (one added for
a specific relationship context, prefixed with the other table's lcode) — and a name passes the
rule if any alternative fully matches. ignore is a list of regexes checked against a name
before it is checked at all (so zz_Archive__Old or a stock Global table are never flagged).
field_global covers fields with FileMaker's Global storage on; field_key covers fields whose
name looks like a key (/^_/ or /id$/i, case-insensitive) and that are actually used on
either side of a relationship — a customerNotes field ending in nothing key-shaped stays a plain
field even if it happens to hold an ID-like value.
saxml4adt conventions --infer # guess one from the majority pattern already in the store
saxml4adt conventions --infer --write # save the guess as saxml4adt.conventions.json (--force to overwrite)
saxml4adt conventions # check every name — the default once the file exists
saxml4adt conventions --kind field -f table # one rule kind only
saxml4adt conventions --strict # exit 1 if anything violates
--infer classifies every existing name into a shape (AAA__Xxx, aaa_AAA__Xxx, xxx, Xxx,
xxx_g, _xxx, XXX, Xxx_Xxx, xxx_xxx, or other), picks the majority shape per rule kind,
and reports a confidence (the share of names that actually match it) plus the top-5 shape
histogram — review those before --writeing an inferred file over a real one. --check (the
default once a file exists and --infer is not given) reports every name that does not match, with
expected (the alternatives it was checked against) and, where trivially derivable — camelCasing a
field, uppercasing a global variable — a suggestion; table and table_occurrence violations get
no suggestion, since picking the right 3-letter code is a human call.
Multi-file projects
A solution split across several .fmp12 files — a UI file plus one or more data files wired
together with external data sources — builds into one store holding all of them, with
references resolved across the file boundary: a field in a data file used only by layouts in the
UI file no longer reads as unreferenced. saxml4adt build --file A --file B (repeatable) or a
saxml4adt.project.json listing "files" in the project root drives which files are ingested;
saxml4adt files lists what the store holds — objects per file, last build, and any unresolved
external data source. Every query that names an object takes --file <name> to narrow it to one
file; left off, the answer covers the whole project — or, once the store holds more than one file
and one is marked primary, the primary file, with --all-files (alias --all) restoring
store-wide scope for that one call. Single-file projects, and multi-file ones with no primary set,
are unaffected — file_id 1 everywhere is exactly the old behavior, byte-identical.
Mark the development file with saxml4adt files --set-primary Empowered_Beginning (or a
"primary" key in saxml4adt.project.json, which wins when both are set; --clear-primary
removes the store's own mark). Once a primary is set, fm-cli write commands
(variable --rename --apply, variables --fix-spelling --apply, uninstall) refuse to target
any other file unless you pass --file <that file> explicitly — the primary is the file you are
developing; the rest are treated read-only from here. export/install/uninstall --dry-run
still work per-file regardless, since pulling or installing the export machinery on an old file is
harmless.
build also guards against copy-twins: a file whose export shares more than 20% of its object
uuids with a different file already in the store — the same file present twice under different
names, e.g. an old export left next to a newer one — is held for confirmation (or, without a
terminal, refused with exit 3) unless --allow-twin (alias --yes) is passed; saxml4adt files
reports any such pair it finds, with the overlap percentage.
Full design in docs/MULTI-FILE.md.
Boundaries
- Read-only.
fm-cliremains the only writer. - Structure only. A table can be wired everywhere and hold zero records; this tool cannot see data, whether layout objects render, or business facts.
unreferenced≠ unused. Manual-by-design scripts, external callers (Data API, WebDirect), and by-name runtime addressing are invisible here; targets that could be addressed dynamically are reportedUNDECIDABLE.
Known FileMaker limits (found while building this)
- Save-as-XML can omit objects fm-cli appended to an existing layout —
the live layout has them, the export does not.
saxml4adt verify-layoutscross-checks every layout's object count against the live file through fm-cli. - fm-cli writes a blank modifier name on everything it creates or updates,
and the first export overwrites that blank on layouts with the export session
(
SaXML Export - <account> <pid>). Details:docs/bug-reports/.
Platforms
macOS and Windows (FileMaker Pro's platforms). macOS is what this is developed on; Windows uses the
same code paths — credentials via Windows Credential Manager (keyring), the hook as
saxml4adt hook --as "…", and the container transport, which needs no ssh. Windows has had no
hands-on run yet: if something breaks, saxml4adt init prints the readiness check to include in an
issue. The --transport ssh path assumes a Linux FileMaker Server Documents path (--remote-docs
to override).
Releasing
Tag a version and push it: git tag vX.Y.Z && git push origin vX.Y.Z. The publish workflow builds
the wheel and uploads it to PyPI through trusted publishing (project saxml4adt, environment
pypi) — no tokens in the repo. v0.1.0 is on PyPI now, which is why Install above leads with
pipx install saxml4adt; the next tag replaces it the same way.
Development
git clone https://github.com/mw777eds/SaXML4ADT && cd SaXML4ADT
python -m pip install -e '.[dev]'
python -m pytest -q # 190 passed, 7 skipped — the skips need SAXML4ADT_TEST_EXPORT
SAXML4ADT_TEST_EXPORT=/path/to/an/export python -m pytest -q # + those against a real export
Layout: saxml4adt/export.py (find and repair the XML), calc.py (tokenize
calcs), steps.py (decode script steps), ingest.py (build the store),
queries.py (every question), conventions.py (naming-conventions check + infer),
cli.py (typer app: commands, export transports, hook, init, install),
web.py + web/index.html (console), mcp.py (MCP stdio server).
Metadata
Release files for saxml4adt 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| saxml4adt-0.1.1.tar.gz | 484.6 kB | With provenance | Details for saxml4adt-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| saxml4adt-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for saxml4adt-0.1.1-py3-none-any.whl |
Total release size: 855.2 kB
Release files / saxml4adt-0.1.1.tar.gz
| Download URL | saxml4adt-0.1.1.tar.gz |
|---|---|
| Size | 484.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bdf724ead583c003200905cdd894dc1e4e0a16c7502429863aacf2674ecc0758
|
|
BLAKE2b-256 checksum How to use checksums |
2362524f266cf60ffd330876b56bdbb21b04f50c16d00190420f97d4bcbf923c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logRelease files / saxml4adt-0.1.1-py3-none-any.whl
| Download URL | saxml4adt-0.1.1-py3-none-any.whl |
|---|---|
| Size | 370.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
62b1ec7dd689b79982cd6ee4cad450934119fac17681aeb4f4d53f364bba070d
|
|
BLAKE2b-256 checksum How to use checksums |
18028b67dea384f06bb1b0bfd96f49b165463a4b700108ac3a0a6faa778ac5c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logSecurity for saxml4adt 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for saxml4adt-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.