.agents/skills/actions/SKILL.md
How to create and run agent actions. Actions are the single source of truth for app operations — the agent calls them as tools, the frontend calls them as HTTP endpoints. Use when creating a new action, adding an API integration, or wiring up frontend data fetching.
npx skillsauth add BuilderIO/agent-native actionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Actions in actions/ are the single source of truth for app operations. The agent calls them as tools, and the framework auto-exposes them as HTTP endpoints at /_agent-native/actions/:name. The frontend calls those endpoints using React Query hooks. No duplicate /api/ routes needed.
Actions give the agent callable tools with structured input/output, AND they give the frontend type-safe HTTP endpoints automatically. 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.
Use defineAction with a Zod schema (required for new actions):
// 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.
Tips:
.describe() for parameter descriptions.optional() for optional paramsz.coerce.number() / z.coerce.boolean() for params that arrive as strings from HTTPz.enum(["draft", "published"]) for constrained valuesThe legacy parameters field (plain JSON Schema object) still works as a fallback but does not provide runtime validation or type inference.
http OptionControls 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 |
The framework auto-refreshes the UI after any successful mutating action. On completion of a non-GET action, the server emits a poll event 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.
Rules:
http: { method: "GET" } → read-only, does NOT trigger a refresh (inferred automatically).POST, PUT, DELETE, or http: false) → treated as mutating, triggers a refresh on success.POST that only reads), pass readOnly: true on the action definition.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.
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.
// 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);
}
The frontend calls action endpoints using React Query hooks from @agent-native/core/client:
useActionQuery — for GET actionsimport { 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 actionsimport { 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.
pnpm action my-action --input data/source.json --output data/result.json
The default template uses core's runScript() in actions/run.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).
/api/ RoutesMost operations should be actions. You only need custom routes in server/routes/api/ for:
If it's a standard CRUD operation or data query, use an action instead.
Older actions use a bare async function export with parseArgs:
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.
JSON.stringify().http: { method: "GET" } for read-only actions. Default is POST.http: false for agent-only actions (navigate, view-screen).loadEnv() if the action needs environment variables (API keys, etc.).fail() for user-friendly error messages (exits with message, no stack trace).@agent-native/core — Don't redefine parseArgs() or other utilities locally.Read action (GET):
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):
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:
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";
},
});
pnpm action foo-bar looks for actions/foo-bar.ts.--key value or --key=value format. Boolean flags use --flag (sets value to "true").http.method doesn't match the hook. Use useActionQuery for GET actions, useActionMutation for POST/PUT/DELETE.JSON.stringify().pnpm action <name>tools
Public booking flow — the state machine, animations, and URL/app-state sync.
tools
Trigger-based automations — reminders, follow-ups, webhooks — across the booking lifecycle.
tools
Team event types, round-robin assignment, collective bookings, host weights, and no-show calibration.
development
The pure `computeAvailableSlots` function — inputs, outputs, invariants, and debugging guide.