---
name: actions
description: >-
  How to create and run agent actions. Actions are the single source of truth
  for app operations — the agent calls them as tools and frontend code calls
  them through client hooks. Use when creating a new action, adding an API
  integration, or wiring up frontend data fetching.
metadata:
  internal: true
---

# Agent Actions

## Rule

Actions in `actions/` are the **single source of truth** for app operations. The agent calls them as tools, and the frontend calls them through `useActionQuery` / `useActionMutation`. The framework owns the HTTP transport behind those hooks. No duplicate `/api/` routes needed.

Before creating any custom REST/API route for app data, inspect `actions/` and the action table in `AGENTS.md`. If an action already exists, call it directly from the agent or with `useActionQuery` / `useActionMutation` from the UI. If the capability is missing, create or update a `defineAction`. Do not add `/api/*`, `server/routes/*`, or other pass-through endpoints whose main job is to call, repackage, or re-export an action.

## Why

Actions give the agent callable tools with structured input/output, AND they give the frontend a typed client contract through hooks. One implementation serves both the agent and the UI. They keep the agent's chat context clean, they're reusable, and they can be tested independently.

## Keep the Action Surface Small and Orthogonal

Every agent-exposed action is a tool in the model's context window. There is a real cost to each one: more tools means more for the model to read, disambiguate, and choose between, which degrades tool-selection quality. Treat the action list like an API you have to maintain — add the fewest, most orthogonal actions that cover the capability, not one per UI affordance.

- **Prefer one CRUD-style `update` over N per-field actions.** A single `update-<thing>` that takes a patch of optional fields beats `update-<thing>-name`, `update-<thing>-order`, `update-<thing>-color`, … The agent (and the UI) pass only the fields that change. Same for `create`/`delete` — one orthogonal action per resource, not one per code path.
- **Reach for a generic query / escape hatch before minting a new read action.** If the agent needs more or different data, do not add `get-<thing>-by-x`, `list-<thing>-filtered-by-y`, etc. For provider data, expose the shared `provider-api-catalog` / `provider-api-docs` / `provider-api-request` trio (see `templates/dispatch/actions/`) so the agent can hit any endpoint or filter without a new action each time. For app data in dev, the `db-query` tool already answers arbitrary read questions.
- **Hide UI-only or purely programmatic actions from the model with `agentTool: false`.** An action that only the frontend or an HTTP/cron caller needs should not spend a slot in the model's tool list. `agentTool: false` keeps it callable from `useActionMutation` / `callAction` / `/_agent-native/actions/<name>` while removing it from every agent tool surface (in-app assistant, MCP, A2A).
- **`agentTool: false` is NOT `toolCallable: false`.** They are different switches:
  - `agentTool: false` → hidden from the **model entirely** (it is no longer a tool the agent can see or call). Still frontend/HTTP-callable.
  - `toolCallable: false` → only blocks the **sandboxed extension ("tools") iframe bridge** (`appAction(...)`). The action stays fully visible to the model, the UI, the CLI, MCP, and A2A. Use it for high-blast-radius operations (account/org/auth changes), not for trimming the tool list.
- **Remove or hide stale actions.** When the UI stops using an action, delete it or set `agentTool: false` — do not leave it exposed to the model as dead tool weight. The advisory audit below helps you spot these.

### Audit Script (Advisory)

`pnpm actions:audit [template ...]` (or `node scripts/audit-template-actions.mjs`) statically scans a template's `actions/` and prints two kinds of suggestions:

1. **Likely UI-dead** — HTTP-exposed mutating actions whose name is never referenced under `app/` (candidates to delete or mark `agentTool: false`).
2. **Likely redundant clusters** — groups like `update-foo-name` / `update-foo-order` that could collapse into one orthogonal `update-foo`.

It is **advisory only**: it always exits 0, never fails CI, and uses conservative heuristics, so expect some false positives (e.g. an action the agent calls but the UI doesn't). Use it as a prompt to review, not a gate.

## How to Create an Action

Use `defineAction` with a Zod schema (required for new actions):

```ts
// actions/list-meals.ts
import { z } from "zod";
import { defineAction } from "@agent-native/core";
import { getDb } from "../server/db/index.js";
import { meals } from "../server/db/schema.js";

export default defineAction({
  description: "List all meals",
  schema: z.object({
    date: z.string().describe("Filter by date (YYYY-MM-DD)"),
  }),
  http: { method: "GET" },
  run: async (args) => {
    // args is fully typed: { date: string }
    const db = getDb();
    const rows = await db.select().from(meals);
    return rows; // Return objects/arrays, NOT JSON.stringify()
  },
});
```

The `schema` field accepts a Zod schema (or any Standard Schema-compatible library). It provides runtime validation with clear error messages (400 for HTTP, error result for agent), full TypeScript type inference for `run()` args, and auto-generated JSON Schema for the agent's tool definition. `zod` is a dependency of all templates.

When an action reads or writes app data, use Drizzle's query builder and portable operators from `drizzle-orm`. Do not use raw SQL, `getDbExec()`, or dialect-specific schema imports in normal actions unless there is a documented reason Drizzle cannot express the query.

When an action calls an external service, never hardcode API keys, bearer
tokens, webhook URLs, signing secrets, OAuth refresh tokens, private
Builder/internal data, or customer data. Read user/org/workspace credentials
from `readAppSecret`, `resolveCredential`, OAuth token helpers, or the provider
API credential adapter. Use `process.env` only for explicitly deploy-level
configuration, and keep examples to obvious placeholders.

Tips:
- Use `.describe()` for parameter descriptions
- Use `.optional()` for optional params
- Use `z.coerce.number()` for numeric params that arrive as strings from HTTP.
  For booleans, use an explicit string parser/helper instead of
  `z.coerce.boolean()` because JavaScript treats any non-empty string,
  including `"false"`, as truthy.
- Use `z.enum(["draft", "published"])` for constrained values

The legacy `parameters` field (plain JSON Schema object) still works as a fallback but does not provide runtime validation or type inference.

## Decision Order

When you need app data or a mutation:

1. **Use an existing action** if one already performs the operation.
2. **Create or extend a `defineAction`** when the agent and UI both need a new operation.
3. **Create a custom route only for route-only concerns** such as uploads, streaming, webhooks, OAuth callbacks, or a non-JSON protocol.

Do not build an umbrella REST API to make actions "easier" to call. Actions are already callable by agents, CLIs, React hooks, HTTP, MCP/A2A exposure, and external hosts through the framework.

## Flexible Provider APIs

For provider integrations used in ad hoc analysis, querying, reporting, or
cross-source research, do not hardcode every provider endpoint as a separate
rigid action. Expose the shared provider API action trio instead:

- `provider-api-catalog`: lists provider base URLs, auth style, credential keys,
  docs/spec URLs, placeholders, and examples without exposing secrets.
- `provider-api-docs`: fetches registered provider docs/spec URLs when the
  exact endpoint, filter operator, payload shape, or pagination contract is
  uncertain.
- `provider-api-request`: makes a constrained authenticated HTTP request to the
  provider host, injects configured credentials, blocks private/internal URLs,
  and redacts secrets.

Use `@agent-native/core/provider-api` as the shared substrate. A template should
only add a thin credential adapter when it has app-specific credential lookup
rules. Keep `provider-api-request` `http: false` unless you have a separate UI
permission model for arbitrary provider writes. Specific actions such as
`hubspot-deals`, `search-emails`, or `sync-source` are convenience shortcuts,
not capability limits; agents should fall back to the provider API trio when a
question requires an endpoint or filter that the shortcut does not model.

### The `http` Option

Controls how the action is exposed as an HTTP endpoint:

| Value                     | Behavior                                                    | Use for                          |
| ------------------------- | ----------------------------------------------------------- | -------------------------------- |
| _(omitted)_               | Auto-exposed as `POST /_agent-native/actions/:name`         | Write operations (default)       |
| `{ method: "GET" }`       | Auto-exposed as `GET /_agent-native/actions/:name`          | Read-only queries                |
| `{ method: "PUT" }`       | Auto-exposed as `PUT /_agent-native/actions/:name`          | Update operations                |
| `{ method: "DELETE" }`    | Auto-exposed as `DELETE /_agent-native/actions/:name`       | Delete operations                |
| `{ method: "GET", path: "custom" }` | Auto-exposed as `GET /_agent-native/actions/custom` | Custom route path                |
| `false`                   | Agent-only, never exposed as HTTP                           | `navigate`, `view-screen`, internal actions |

### Screen Refresh (automatic)

The framework auto-refreshes the UI after any successful mutating action. On completion of a non-`GET` action, the framework emits a change event with `source: "action"` that the client's `useDbSync` picks up and uses to invalidate `["action"]` React Query keys — so `list-*` / `get-*` hooks refetch without a full page reload. In-process calls emit directly; dev-mode `pnpm action ...` calls also write a durable marker so the web server sees child-process action changes.

Rules:

- `http: { method: "GET" }` → read-only, does NOT trigger a refresh (inferred automatically).
- Any other action (default `POST`, `PUT`, `DELETE`, or `http: false`) → treated as mutating, triggers a refresh on success.
- To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
- To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.

Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.

### Return Values

Actions should return **structured data** (objects, arrays) — not `JSON.stringify()`. The framework serializes the response automatically. If you return a string, the framework tries to parse it as JSON for a clean response.

```ts
// Good — return structured data
run: async (args) => {
  const events = await fetchEvents(args.from, args.to);
  return events;
}

// Bad — don't stringify
run: async (args) => {
  const events = await fetchEvents(args.from, args.to);
  return JSON.stringify(events, null, 2);
}
```

## Frontend Hooks

The frontend calls actions using React Query hooks from `@agent-native/core/client`. Components should not hand-write `fetch("/_agent-native/actions/...")`; add or reuse a client hook/helper instead. Use `callAction` from the same package for imperative cases that do not fit a hook, such as debounced search, prefetching, or non-React event handlers.

### `useActionQuery` — for GET actions

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

function MealList() {
  // Types are auto-inferred from the action's schema + return type — no manual generic needed
  const { data: meals } = useActionQuery("list-meals", {
    date: "2025-01-01",
  });
  return <ul>{meals?.map((m) => <li key={m.id}>{m.name}</li>)}</ul>;
}
```

### `useActionMutation` — for POST/PUT/DELETE actions

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

function AddMealButton() {
  // Types are auto-inferred — no manual generic needed
  const { mutate } = useActionMutation("log-meal");
  return (
    <button onClick={() => mutate({ name: "Salad", calories: 350 })}>
      Log Meal
    </button>
  );
}
```

**Do NOT use manual type generics** like `useActionQuery<Meal[]>(...)`. Types are inferred automatically from `.generated/action-types.d.ts`, which is auto-generated by a Vite plugin.

Mutations automatically invalidate all `["action"]` query keys on success, so GET queries refetch.

### `callAction` — for imperative client code

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

const people = await callAction("search-people", { query }, { method: "GET" });
```

Prefer hooks in React data flows. Use `callAction` when a hook would be awkward;
do not hand-write action route fetches in components.

## How to Run (Agent)

```bash
pnpm action my-action --input data/source.json --output data/result.json
```

## Action Dispatcher

The default template uses core's `runScript()` in `actions/run.ts`:

```ts
import { runScript } from "@agent-native/core";
runScript();
```

This is the canonical approach for new apps. Action names must be lowercase with hyphens only (e.g., `my-action`).

## When You Still Need Custom `/api/` Routes

Most operations should be actions. You only need custom routes in `server/routes/api/` for:

- **File uploads** — actions receive JSON params, not multipart form data
- **Streaming responses** — SSE or chunked responses that need direct H3 control
- **Webhooks** — external services POST to a specific URL
- **OAuth callbacks** — redirect-based flows that need specific URL patterns

If it's a standard CRUD operation, data query, or a wrapper around an action, use the action instead.

## Legacy Pattern (bare export)

Older actions use a bare async function export with `parseArgs`:

```ts
import { parseArgs, loadEnv, fail } from "@agent-native/core";

export default async function myAction(args: string[]) {
  loadEnv();
  const parsed = parseArgs(args);
  // ...
}
```

This still works but is not auto-exposed as HTTP. Prefer `defineAction` for all new actions.

## Guidelines

- **One action, one job.** Keep actions focused on a single operation. The agent composes multiple action calls for complex operations.
- **Return structured data.** Return objects/arrays, not `JSON.stringify()`.
- **Use `http: { method: "GET" }`** for read-only actions. Default is POST.
- **Use `http: false`** for agent-only actions (`navigate`, `view-screen`).
- **Use `agentTool: false`** for UI-only / programmatic actions that should NOT be a tool in the model's context window. It stays frontend/HTTP-callable but is hidden from the agent. Distinct from `toolCallable: false`, which only blocks the sandboxed extension iframe bridge.
- **Document reusable actions.** If a new action should be called by agents outside one narrow screen, update `AGENTS.md` with when to use it, important args, and which return fields to preserve.
- **Promote workflow-heavy actions to skills.** If the action is part of a provider-backed, cross-app, MCP/A2A, or multi-step workflow, create or update a skill in `.agents/skills/` and add app-skill visibility (`internal`, `exported`, or `both`) when it should ship through a marketplace.
- **Use `loadEnv()`** only for deploy-level configuration. User/org/workspace
  credentials belong in the encrypted secrets/credential/OAuth stores, never as
  hardcoded literals or shared env fallbacks.
- **Use `fail()`** for user-friendly error messages (exits with message, no stack trace).
- **Import from `@agent-native/core`** — Don't redefine `parseArgs()` or other utilities locally.
- **Do not re-export actions as REST.** The mounted `/_agent-native/actions/:name` endpoint is the REST surface; duplicating it under `/api/*` creates drift and hides the operation from agents.

## Common Patterns

**Read action (GET):**

```ts
import { z } from "zod";
import { defineAction } from "@agent-native/core";

export default defineAction({
  description: "List calendar events",
  schema: z.object({
    from: z.string().describe("Start date"),
    to: z.string().describe("End date"),
  }),
  http: { method: "GET" },
  run: async (args) => {
    return await fetchEvents(args.from, args.to);
  },
});
```

**Write action (POST, default):**

```ts
import { z } from "zod";
import { defineAction } from "@agent-native/core";

export default defineAction({
  description: "Log a meal",
  schema: z.object({
    name: z.string().describe("Meal name"),
    calories: z.coerce.number().describe("Calorie count"),
  }),
  run: async (args) => {
    // args.calories is a number — z.coerce.number() handles string-to-number conversion from HTTP
    const meal = await insertMeal(args);
    return meal;
  },
});
```

**Agent-only action:**

```ts
import { z } from "zod";
import { defineAction } from "@agent-native/core";

export default defineAction({
  description: "Navigate the UI to a view",
  schema: z.object({
    view: z.string().describe("Target view"),
  }),
  http: false,
  run: async (args) => {
    await writeAppState("navigate", { command: "go", view: args.view });
    return "Navigated";
  },
});
```

## Troubleshooting

- **Action not found** — Check that the filename matches the command name exactly. `pnpm action foo-bar` looks for `actions/foo-bar.ts`.
- **Args not parsing** — Ensure args use `--key value` or `--key=value` format. Boolean flags use `--flag` (sets value to `"true"`).
- **Frontend getting 405** — The action's `http.method` doesn't match the hook. Use `useActionQuery` for GET actions, `useActionMutation` for POST/PUT/DELETE.
- **Frontend getting undefined** — Make sure the action returns structured data, not `JSON.stringify()`.

## Related Skills

- **storing-data** — Actions read/write data in SQL
- **delegate-to-agent** — The agent invokes actions via `pnpm action <name>`
- **real-time-sync** — Database writes from actions trigger change events to update the UI
- **adding-a-feature** — Actions are area 2 of the four-area checklist
- **client-methods** — Client code uses named helpers/hooks instead of raw REST calls
