Data & Sync
The primary way to read and write app data from client application is through action hooks. By embedding requests into actions, both your client and agent can interact with your application in the same way.
In the example below, a ticket is assigned to the current user manually through a dropdown, and then through the chat agent. Both the UI and agent access the same action.
Example of a UI action and an agent action having the same result.
Fetch and mutate data
A typical app builds its own API layer and calls it with hand-written fetch requests. An agent-native app skips that layer: app/ calls the same actions the agent already has as tools, through the hooks below.
A typical component calls a route only it can reach. An agent-native component calls an action the agent can call too.
Action hooks
There are two action hooks you are most likely to use: useActionQuery() and useActionMutation(). These two hooks provide access to your app's actions.
useActionQuery
Function signature
This hook is intended to be used for read-only actions. It is a thin wrapper around the useQuery() function.
In the example below, the list-tickets action is called. This is similar to making a GET request for all tickets.
import { useActionQuery } from "@agent-native/core/client/hooks";
const { data, isLoading } = useActionQuery("list-tickets", {});The following example shows a request to fetch a single ticket, through the get-ticket action. A ticketId is provided to scope the query to only return a specific ticket. The additional enabled option is provided to ensure the query does not automatically run, and instead only runs when a ticketId is present.
import { useActionQuery } from "@agent-native/core/client/hooks";
const { data, isLoading } = useActionQuery(
"get-ticket",
{ ticketId: ticketId ?? "" },
{ enabled: Boolean(ticketId) },
);useActionMutation
Function signature
This hook is intended to be used for actions that change data, like creating, updating, or sending something. It is a thin wrapper around the useMutation() function.
Unlike useActionQuery(), the action's input is not passed to the hook itself. It is passed to mutate() or mutateAsync() when you are ready to run the mutation.
In the example below, the send-ticket-reply action is called to post a reply to a ticket. Call mutate() from an event handler, such as a button's onClick.
import { useActionMutation } from "@agent-native/core/client/hooks";
const { mutate, isPending } = useActionMutation("send-ticket-reply");
mutate({ ticketId, body: draft });The following example shows the same mutation with an onSuccess option. Cache invalidation happens automatically after every mutation, so onSuccess is only needed for local side effects, like clearing the reply composer once the reply is sent.
import { useActionMutation } from "@agent-native/core/client/hooks";
const { mutate, isPending } = useActionMutation("send-ticket-reply", {
onSuccess: () => setDraft(""),
});
mutate({ ticketId, body: draft });callAction
Function signature
This function is for imperative one-off action calls outside of a React render flow, such as a plain event handler, a script, or any other non-React code path. It returns a promise directly instead of a hook result.
In the example below, the update-ticket action is called from a keyboard shortcut handler outside of React's render flow, assigning the ticket to a rep.
import { callAction } from "@agent-native/core/client/hooks";
await callAction("update-ticket", {
ticketId,
assignedToEmail: "jordan@example.com",
});The following example shows the same call with a timeoutMs option, useful for actions that legitimately take longer than the default 60 second timeout.
import { callAction } from "@agent-native/core/client/hooks";
await callAction(
"update-ticket",
{ ticketId, assignedToEmail: "jordan@example.com" },
{ timeoutMs: 120_000 },
);Every useActionQuery(), useActionMutation(), and callAction() request is bounded by a 60 second timeout, and timeouts surface as errors instead of retrying silently. useActionQuery() also cancels superseded requests automatically, using React Query's abort signal.
useDbSync
Function signature
useDbSync() always handles local chat-run tool completions and the run-end refresh. That keeps single-user agent edits current without a background request. Background sync for changes from other tabs, users, webhooks, or jobs is off unless this page opts in with a short reason. An opted-in page shares one transport per tab, pauses it while hidden, and backs idle polls off from 1 to 2 to 5 minutes. On a production serverless deploy, /_agent-native/events answers immediately and polling carries remote sync.
import { useDbSync } from "@agent-native/core/client/hooks";
import { useQueryClient } from "@tanstack/react-query";
function App() {
const queryClient = useQueryClient();
useDbSync({
queryClient,
realtime: { reason: "teammates can edit this shared record" },
pollUrl: "/_agent-native/poll",
onEvent: (data) => console.log("Data changed:", data),
});
return <div>...</div>;
}Use realtime only when data can change without this user acting and the user needs to see it before refreshing. Shared Slides editing and a Mail inbox are good fits. Public pages, docs, settings, forms, read-mostly lists, and single-user agent edits should stay opted out; use focus/navigation refetches and the chat run stream instead. Each opted-in open tab adds one poll per idle interval. For normal CRUD, prefer useActionQuery() and useActionMutation(). If a raw useQuery() call needs to sync on a custom key, see Sync Internals. If you need to write to application_state directly, see Advanced.
What's next
- Overview — the
app/andpublic/directory layout this page's hooks live in - Agent Chat — sending prompts and staging context for the agent
- Actions — the
defineActionsurface behind every hook on this page - Advanced — direct
application_stateaccess - Sync Internals — syncing a raw
useQuerywithuseChangeVersion - Real-Time Sync — how
useDbSynckeeps the UI current