Call FileMaker scripts from an app
A Web Viewer app has two ways to talk to FileMaker: the Data API path (typed CRUD against a layout, covered in Use a typed data client) and FileMaker scripts. Scripts are the backend for everything that isn't a straightforward record read or write — the Web Viewer handles the interface, and a script performs privileged work inside FileMaker on its behalf. For guidance on when to reach for a script instead of Data API CRUD, see Splitting App Logic.
Calling a script from the Web Viewer
@adt/fmdapi exports two functions for this, both re-exported from the top-level package:
-
fmFetch(scriptName, data)— calls a script and returns aPromisethat resolves with whatever the script sends back. Use this whenever the app needs a result. -
callFMScript(scriptName, data, option?)— fires a script and returns immediately, with no result. Use it for navigation or "just do it" actions, with an optionalFMScriptOption(CONTINUE,HALT,EXIT,RESUME,PAUSE,SUSPEND_AND_RESUME) when the script needs a specific pause/resume behavior.
import { fmFetch } from "@adt/fmdapi";
type DashboardResult = {
openInvoices: number;
overdueInvoices: number;
};
const dashboard = await fmFetch<DashboardResult>("Get Dashboard", {
accountId: "123",
});
import { FMScriptOption, callFMScript } from "@adt/fmdapi";
callFMScript(
"Navigate Away From Web Viewer",
{ layout: "Contacts" },
FMScriptOption.RESUME,
);
fmFetch hands your data object straight to the script as its script parameter — no envelope, no wrapper. An object arrives as that object, an array as that array, a string verbatim:
{ "accountId": "123" }
That means the script reads its inputs from the top level, not nested under a data key:
Set Variable [ $param ; Get ( ScriptParameter ) ]
Set Variable [ $account ; JSONGetElement ( $param ; "accountId" ) ] // correct — top level
Set Variable [ $wrong ; JSONGetElement ( $param ; "data.accountId" ) ] // WRONG — returns ""
The script sends its result back with Exit Script [ Result ]. fmFetch performs the script through window.FileMaker.PerformScriptAsync and resolves with whatever the script exits with — a JSON result is parsed, a bare string is passed through as-is, and an empty result resolves as undefined.
This request/response plumbing — full signatures, the FMScriptOption values, and the result contract in detail — is covered in Script Calls. This page stays at the "how to think about it" level.
What the script must do
A script called through fmFetch owns the app-specific success, validation-error, and unexpected-error contract. A few rules keep that contract honest:
-
Read inputs from the top level, as shown above.
-
Exit with a result on every code path — including validation failures and trapped errors. A script that halts before reaching
Exit Scriptleaves the Web Viewer'sfmFetchpromise pending forever;fmFetchhas no timeout. -
Never return success unconditionally. If the script delegates work — Perform Script on Server, a transaction subscript — check the result and
Get(LastError)before reporting success. A wrapper that ignores a failed delegated call and reports success anyway produces "success with no write," which is very hard to debug from the JavaScript side. -
Return a consistent result your app can validate, such as
{ ok, message }or{ messages: [...], response: {...} }. There's no universalfmFetchresult shape — each app defines its own, and the calling code should validate it rather than assume it. -
Never navigate the Web Viewer's own window. The script runs in the context of the Web Viewer's window — don't change its layout or found set. If the work needs record context, do it in an off-screen New Window, or hand it to Perform Script on Server.
import { fmFetch } from "@adt/fmdapi";
import { z } from "zod/v4";
const saveResultSchema = z.object({
ok: z.boolean(),
message: z.string().optional(),
});
const result = saveResultSchema.parse(
await fmFetch("Save Contact Preferences", { contactId: "123" }),
);
Shape of a script called from the app
A script written to be called through fmFetch follows the same basic skeleton, whatever it does:
Set Variable [ $param ; Get ( ScriptParameter ) ]
# Read whatever fields the call needs, at the top level
Set Variable [ $accountId ; JSONGetElement ( $param ; "accountId" ) ]
# Do the work. If record context is needed, do it in an off-screen
# New Window rather than the Web Viewer's own window — or hand it
# to Perform Script on Server.
Set Variable [ $result ; JSONSetElement ( "{}" ; "ok" ; True ; JSONBoolean ) ]
Exit Script [ Text Result: $result ]
Write your business logic in place of the middle step, and make sure every branch — including error and validation paths — reaches its own Exit Script [ Result ] rather than falling through to a halt.
Related docs
-
Splitting App Logic — when to use a script instead of Data API CRUD.
-
Script Calls — full
fmFetch/callFMScriptAPI reference. -
Use a typed data client — the Data API CRUD path, for straightforward record reads and writes.
-
How ADT Connects to FileMaker — how scripts, the bridge, and the Web Viewer fit together.