---
title: "Data & Sync"
description: "The action hooks app/ uses to read, write, and stay live-synced with the same actions the agent calls."
---

# Data & Sync

The primary way to read and write app data from client application is through [action](/docs/actions-overview) 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.

<Video
  src="https://cdn.builder.io/o/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fa5548a35d1494c1f8a4f960fbe8d9b2c%2Fcompressed?apiKey=YJIGb4i01jvw0SRdL5Bt&token=a5548a35d1494c1f8a4f960fbe8d9b2c&alt=media&optimized=true"
  alt="Example of a UI action and an agent action having the same result."
  align="full"
  autoplay={true}
  loop={true}
  width={600}
  caption="Example of a UI action and an agent action having the same result."
/>

## Fetch and mutate data {#fetching-mutating}

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.

<Diagram id="doc-block-client-data-fm-1" title="Typical fetch vs. an action hook" summary="A typical component calls a route only it can reach. An agent-native component calls an action the agent can call too.">

```html
<div class="diagram-fmcmp">
  <div class="fmcmp-col">
    <div class="fmcmp-label diagram-muted">Typical</div>
    <div class="diagram-node">Component</div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-node">fetch("/api/leads/:id")</div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-panel" data-rough>
      <strong>Your API route</strong>
      <small class="diagram-muted">only the frontend can call it</small>
    </div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-box" data-rough>Database</div>
  </div>
  <div class="fmcmp-vs diagram-muted">vs</div>
  <div class="fmcmp-col">
    <div class="fmcmp-label diagram-muted">Agent-Native</div>
    <div class="diagram-node">Component</div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-node">useActionQuery("get-lead")</div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-panel" data-rough>
      <span class="diagram-pill accent">get-lead action</span>
      <small class="diagram-muted">also an agent tool</small>
    </div>
    <div class="fmcmp-arrow diagram-muted" aria-hidden="true">&#8595;</div>
    <div class="diagram-box" data-rough>SQL database</div>
  </div>
</div>
```

```css
.diagram-fmcmp {
  display: flex;
  align-items: flex-start;
  gap: 28px;
  flex-wrap: wrap;
}
.fmcmp-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 200px;
}
.fmcmp-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
}
.fmcmp-vs {
  font-size: 13px;
  font-weight: 600;
  padding-top: 52px;
}
.diagram-fmcmp .diagram-panel {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 16px;
  align-items: center;
}
.fmcmp-arrow {
  font-size: 18px;
  line-height: 1;
}
```

</Diagram>

### Action hooks {#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 {#use-action-query}

<Signature
  id="doc-block-client-data-1"
  name="useActionQuery"
  params={[
    {
      name: "actionName",
      type: "TName",
      description: "The action's registered name.",
    },
    {
      name: "params",
      type: "ActionParams<TName>",
      optional: true,
      description:
        "The action's input, typed from its `defineAction()` schema.",
    },
    {
      name: "options",
      type: "UseQueryOptions",
      optional: true,
      description:
        "Everything from the [useQuery options](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) except `queryKey` and `queryFn`, which the hook sets itself.",
    },
  ]}
  returns={{
    type: "UseQueryResult<TResult>",
    description:
      'Cached under `["action", name, params]`. Refetches automatically whenever a mutating action completes, with no manual invalidation needed.',
  }}
/>

This hook is intended to be used for read-only actions. It is a thin wrapper around the [useQuery()](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) function.

In the example below, the `list-tickets` action is called. This is similar to making a GET request for all tickets.

```ts
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.

```ts
import { useActionQuery } from "@agent-native/core/client/hooks";

const { data, isLoading } = useActionQuery(
  "get-ticket",
  { ticketId: ticketId ?? "" },
  { enabled: Boolean(ticketId) },
);
```

#### useActionMutation {#use-action-mutation}

<Signature
  id="doc-block-client-data-2"
  name="useActionMutation"
  params={[
    {
      name: "actionName",
      type: "TName",
      description: "The action's registered name.",
    },
    {
      name: "options",
      type: "UseMutationOptions",
      optional: true,
      description:
        "Everything from the [useMutation options](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) except `mutationFn`, which the hook sets itself.",
      fields: [
        {
          name: "method",
          type: '"POST" | "PUT" | "DELETE"',
          optional: true,
          default: '"POST"',
          description: "Overrides the default request method.",
        },
        {
          name: "skipActionQueryInvalidation",
          type: "boolean",
          optional: true,
          default: "false",
          description: "Opts out of the automatic cache refresh on success.",
        },
      ],
    },
  ]}
  returns={{
    type: "UseMutationResult",
    description:
      'On success, emits a change event with `source: "action"`, so every `useActionQuery()` reading affected data refetches automatically. No manual invalidation is needed.',
  }}
/>

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()](https://tanstack.com/query/latest/docs/framework/react/reference/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`.

```ts
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.

```ts
import { useActionMutation } from "@agent-native/core/client/hooks";

const { mutate, isPending } = useActionMutation("send-ticket-reply", {
  onSuccess: () => setDraft(""),
});

mutate({ ticketId, body: draft });
```

### callAction {#call-action}

<Signature
  id="doc-block-client-data-3"
  name="callAction"
  params={[
    {
      name: "actionName",
      type: "TName",
      description: "The action's registered name.",
    },
    {
      name: "params",
      type: "ActionParams<TName>",
      optional: true,
      description:
        "The action's input, typed from its `defineAction()` schema.",
    },
    {
      name: "options",
      type: "ClientActionCallOptions",
      optional: true,
      description: "Everything below is optional.",
      fields: [
        {
          name: "method",
          type: '"GET" | "POST" | "PUT" | "DELETE"',
          optional: true,
          default: '"POST"',
          description: "Overrides the default request method.",
        },
        {
          name: "signal",
          type: "AbortSignal",
          optional: true,
          description: "Cancels the request.",
        },
        {
          name: "timeoutMs",
          type: "number",
          optional: true,
          default: "60000",
          description:
            "Overrides the default 60 second timeout, for actions that legitimately take longer.",
        },
      ],
    },
  ]}
  returns={{
    type: "Promise<TResult>",
    description:
      "Unlike the hooks above, there is no cache and no automatic refetch. Pair it with a manual invalidation if other UI depends on the result.",
  }}
/>

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.

```ts
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.

```ts
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 {#usedbsync}

<Signature
  id="doc-block-client-data-4"
  name="useDbSync"
  params={[
    {
      name: "options",
      type: "UseDbSyncOptions",
      description: "Every field is optional.",
      fields: [
        {
          name: "queryClient",
          type: "QueryClient",
          optional: true,
          description: "React Query client used for cache invalidation.",
        },
        {
          name: "pollUrl",
          type: "string",
          optional: true,
          default: '"/_agent-native/poll"',
          description: "Poll endpoint URL.",
        },
        {
          name: "realtime",
          type: "{ reason: string }",
          optional: true,
          description:
            "Opt this page into background change sync. A short reason is required so the cost and use case are visible in code. Without it, no background sync transport is opened.",
        },
        {
          name: "sseUrl",
          type: "string | false",
          optional: true,
          default: '"/_agent-native/events"',
          description: "SSE endpoint URL. Pass `false` to use polling only.",
        },
        {
          name: "interval",
          type: "number",
          optional: true,
          default: "2000",
          description:
            "Polling interval in ms while an agent run is active. Idle polling backs off to 1, 2, then 5 minutes.",
        },
        {
          name: "fallbackInterval",
          type: "number",
          optional: true,
          default: "60000",
          description:
            "Minimum idle poll interval in ms while SSE is connected. Successful empty polls back off from 1 to 2 to 5 minutes.",
        },
        {
          name: "pauseWhenHidden",
          type: "boolean",
          optional: true,
          default: "true",
          description: "Pause background sync while the browser tab is hidden.",
        },
        {
          name: "ignoreSource",
          type: "string",
          optional: true,
          description:
            "Per-tab request source to ignore, so a tab does not refetch from its own writes.",
        },
        {
          name: "onEvent",
          type: "(data) => void",
          optional: true,
          description:
            "Optional callback for remote sync events and local chat-run tool completions.",
        },
        {
          name: "actionInvalidatePredicate",
          type: "(query: Query) => boolean",
          optional: true,
          description:
            "Filters the broad invalidation an action event triggers. Rarely needed. Use it to keep an expensive active query on explicit-refresh semantics while normal source-versioned queries still react through `useChangeVersion`.",
        },
        {
          name: "suppressActionInvalidationFor",
          type: "string[]",
          optional: true,
          description:
            "Action names whose sync events should not invalidate all action queries. Only for high-volume background actions that already perform their own narrow invalidation.",
        },
        {
          name: "queryKeys",
          type: "string[]",
          optional: true,
          description: "Deprecated and ignored. Kept for old call sites.",
        },
      ],
    },
  ]}
  returns={{ type: "void" }}
/>

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

```ts
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](/docs/client-sync-internals). If you need to write to `application_state` directly, see [Advanced](/docs/client-advanced).

## What's next

- [**Overview**](/docs/client-overview) — the `app/` and `public/` directory layout this page's hooks live in
- [**Agent Chat**](/docs/client-agent-chat) — sending prompts and staging context for the agent
- [**Actions**](/docs/actions-overview) — the `defineAction` surface behind every hook on this page
- [**Advanced**](/docs/client-advanced) — direct `application_state` access
- [**Sync Internals**](/docs/client-sync-internals) — syncing a raw `useQuery` with `useChangeVersion`
- [**Real-Time Sync**](/docs/key-concepts#polling-sync) — how `useDbSync` keeps the UI current
