Use a typed data client
Agentic Development Toolkit (ADT) gives a Web Viewer app type-safe access to FileMaker records through a generated client. This page covers using that client after Typegen creates it.
Each app is bound to exactly one connected file, so its generated client only talks to that file.
Note New to these tools? This page assumes some web vocabulary — Zod schemas, TanStack Query, state, validation. The Terminology defines each term and links to its documentation.
The library: @adt/fmdapi
The client is built on @adt/fmdapi, a single library that bundles the Data API client, the Web Viewer adapter, and an agent skill for using it correctly. Unlike a typical npm dependency, it is vendored into your project at vendor/@adt/fmdapi the first time you run adt app add — it isn't installed from the npm registry. There's one shared copy per project, and every app resolves it through the pnpm workspace.
Because it's vendored, pnpm update won't touch it. Ask your agent to move it to a newer ADT build.
@adt/fmdapi exports DataApi, WebViewerAdapter, fmFetch, callFMScript, and React helpers. Typegen generates a client that wires the layout, schema, and adapter together.
Note Generate the client first See Generate types with Typegen for layout configuration, generation, output files, and regeneration after FileMaker changes.
Use the generated client
Import the generated type and client instead of hand-writing either:
import type { TContacts } from "@/config/schemas/filemaker/Contacts";
import { ContactsLayout } from "@/config/schemas/filemaker/client";
const response = await ContactsLayout.find({
query: { Status: "Active" },
limit: 25,
});
const firstContact: TContacts | undefined = response.data[0]?.fieldData;
find(), get(), and list() all return the same shape: data is always an array of { fieldData, recordId, modId }, even get() for a single record by id. Only findOne, findFirst, and maybeFindFirst return a singular record directly.
A find that matches nothing throws a FileMaker error 401 by default. Pass ignoreEmptyResult: true whenever an empty result is a normal outcome:
const response = await ContactsLayout.find({
query: { Status: "Archived" },
ignoreEmptyResult: true,
});
// response.data is [] when nothing matches
Mutations use the same generated client:
await ContactsLayout.update({
recordId: 42,
fieldData: { Status: "Inactive" },
});
Load data with TanStack Query
The project template includes TanStack Query. Prefer it over a hand-rolled useEffect fetch, since FileMaker data often needs refetching after a script runs or a mutation lands:
import { ContactsLayout } from "@/config/schemas/filemaker/client";
import { useQuery } from "@tanstack/react-query";
export function useContactsPage(page: number) {
return useQuery({
queryKey: ["contacts", { page }],
queryFn: () =>
ContactsLayout.find({ query: { Status: "Active" }, limit: 25, offset: page * 25 + 1 }),
});
}
Use FileMaker scripts for transactional changes
Data API CRUD calls are not transactional — batching several client calls together does not make them atomic. When multiple related records must change together, or record locking matters, call a FileMaker script instead; see Splitting App Logic for when to reach for a script over Data API CRUD.
import { fmFetch } from "@adt/fmdapi";
await fmFetch("Deactivate Contact And Related Orders", { contactId: 42 });
Common mistakes
-
Treating
get()as returning a single record. It still returnsdata: [...]— useresponse.data[0]?.fieldData, or usefindFirst/maybeFindFirstwhen you need a singular result. -
Letting an empty find crash the app. Pass
ignoreEmptyResult: trueon any find where zero matches is expected. -
Bypassing the generated client. Constructing your own
DataApi+WebViewerAdapterinstead of using the generated<Schema>Layoutclient throws away the type safety typegen exists to provide. -
Redefining generated types by hand. Import
T<Schema>(or infer fromZ<Schema>) instead of writing a parallel type that can drift from the real layout. -
Loading unbounded record sets.
listAll/findAllcan pull an unknown number of records — prefer paginatedfind/listcalls with an explicitlimitandoffset.
Related guides
-
Generate types with Typegen: configure layouts and regenerate the client.
-
Deploy to FileMaker: bundle the generated client with the app.
-
@adt/fmdapireference: the complete client API. -
adt.config.json: every Typegen configuration field.