Web Viewer commands and React hooks

Everything else in this reference covers your app asking FileMaker for something. Commands are the other direction: a FileMaker script reaches into the already-running Web Viewer app and calls a function you registered ahead of time — refresh a screen, open a record, show a toast, navigate a route.

For the app-calls-FileMaker direction, see Script Calls.

How FileMaker triggers a command

FileMaker calls into the Web Viewer with the Perform JavaScript in Web Viewer script step. That step takes the target Web Viewer object name, a JavaScript function name, and one or more parameters. Every parameter FileMaker passes arrives as a string.

Commands you register live on a global object on window, keyed by namespace (window.adt by default). A script step that calls adt.openCustomer with a parameter of 123 invokes the openCustomer handler you registered with "123" as its argument. Because FileMaker only ever hands over strings, command handlers must accept string parameters — if a handler needs a number or an object, parse or JSON.parse it inside the handler.

Initialize

Call initWebViewerCommands() once, early in your app's startup (for example in main.tsx), before any FileMaker script tries to call a command.

Copy
initWebViewerCommands();

The module is import-safe outside the browser: initWebViewerCommands() returns undefined when window is unavailable, so it's safe to call during a server-side build step.

Type commands

Declare your commands in a .d.ts file by extending WebViewerCommandRegistry in the @adt/fmdapi/commands module. DefineWebViewerCommandRegistry checks at compile time that every command is a function that only takes string parameters.

Copy
export {};

declare module "@adt/fmdapi/commands" {
  interface WebViewerCommandRegistry
    extends DefineWebViewerCommandRegistry<{
      openCustomer: (recordId: string) => void;
      refreshDashboard: () => void;
    }> {}
}

You can also wrap an individual entry with WebViewerCommandHandler instead of extending the whole registry through DefineWebViewerCommandRegistry:

Copy
declare module "@adt/fmdapi/commands" {
  interface WebViewerCommandRegistry {
    openCustomer: WebViewerCommandHandler<(recordId: string) => void>;
  }
}

registerWebViewerCommand and useWebViewerCommand both use this registry to check command names and parameter types, so a typo in a command name or a non-string parameter fails at compile time.

Static commands

Use registerWebViewerCommand for a handler that doesn't depend on component state — a module-level listener your app registers once. This example lets FileMaker ask the Web Viewer to load a customer summary through fmFetch and broadcast the result:

Copy
registerWebViewerCommand("loadCustomerSummary", async (recordId) => {
  const summary = await fmFetch("Load Customer Summary", { recordId });
  window.dispatchEvent(
    new CustomEvent("customer-summary-loaded", {
      detail: summary,
    }),
  );
});

registerWebViewerCommand calls initWebViewerCommands() internally if you haven't already, so it's safe to call on its own. It returns a cleanup function that unregisters the handler:

Copy
const unregister = registerWebViewerCommand("loadCustomerSummary", handler);
// later
unregister();

React commands

Use useWebViewerCommand from @adt/fmdapi/react when a handler needs UI state, routing, or component data.

Copy
export function CustomerScreen() {
  useWebViewerCommand("openCustomer", (recordId) => {
    console.log(recordId);
  });

  return null;
}

The hook registers the command on mount and unregisters it on unmount, and it always calls the latest render's callback — you don't need useCallback to keep it current.

Missing-command behavior

If FileMaker calls a command before your app has registered it — a common race during startup — the default behavior is to buffer the call and replay it once the handler registers:

Copy
initWebViewerCommands({
  missingCommand: "buffer",
  maxBufferedCallsPerCommand: 100,
});

missingCommand also accepts "drop" (silently ignore), "warn" (log a warning), and "throw" (throw an error). maxBufferedCallsPerCommand bounds the buffer per command name; once it's full, the oldest buffered call is dropped with a warning.

Multiple namespaces and inspecting the registry

By default every command lives under window.adt. Pass namespace to initWebViewerCommands if you need a different or additional global (useful for isolating command sets, or in tests):

Copy
initWebViewerCommands({ namespace: "reportsApp" });

getWebViewerCommandRegistry() returns a handle for the active namespace so you can inspect or clear pending calls, which is useful when debugging a command that never seems to fire:

Copy
const registry = getWebViewerCommandRegistry();
registry?.listRegisteredCommands(); // e.g. ["openCustomer", "refreshDashboard"]
registry?.listPendingCommands(); // commands FileMaker called before they registered

unregisterWebViewerCommand(name) removes a handler outside of the cleanup function returned by registerWebViewerCommand, if you need to tear one down explicitly.

Note  Skip direct window assignment Registering commands by assigning window.adt.openCustomer = ... directly bypasses buffering, error handling, and the type checks the registry gives you. allowDirectAssignment defaults to false for exactly this reason — use registerWebViewerCommand or useWebViewerCommand instead.