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:

Copy
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:

Copy
const response = await ContactsLayout.find({
  query: { Status: "Archived" },
  ignoreEmptyResult: true,
});
// response.data is [] when nothing matches

Mutations use the same generated client:

Copy
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:

Copy
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.

Copy
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 returns data: [...] — use response.data[0]?.fieldData, or use findFirst/maybeFindFirst when you need a singular result.

  • Letting an empty find crash the app. Pass ignoreEmptyResult: true on any find where zero matches is expected.

  • Bypassing the generated client. Constructing your own DataApi + WebViewerAdapter instead of using the generated <Schema>Layout client throws away the type safety typegen exists to provide.

  • Redefining generated types by hand. Import T<Schema> (or infer from Z<Schema>) instead of writing a parallel type that can drift from the real layout.

  • Loading unbounded record sets. listAll/findAll can pull an unknown number of records — prefer paginated find/list calls with an explicit limit and offset.