---
title: "Creating Templates"
description: "How to create and publish your own agent-native app templates."
---

# Creating Templates

Templates are complete, cloneable agent-native apps that solve a real workflow. The first-party templates are built with the same framework surface you use: React routes for the UI, Drizzle SQL for data, actions for operations, workspace resources for agent behavior, and polling sync so the agent and UI stay aligned.

Use `@agent-native/core` for the runtime contract: actions, server plugins,
database helpers, app state, routing helpers, and agent chat. Use
`@agent-native/toolkit` for reusable app-building UI and helpers such as shared
shadcn-style primitives, toast/mobile hooks, date pickers, and simple app shell
slots. Keep template-specific variants local when they encode product behavior
or visual identity.

A good template:

- Solves one workflow end-to-end, with useful seed data or an empty-state flow.
- Stores durable state in SQL, not JSON files.
- Defines app operations as `defineAction()` actions.
- Exposes navigation and selection through application state.
- Ships a clear `AGENTS.md` plus focused skills for non-obvious workflows.
- Registers onboarding steps for required providers and secrets.
- Works as a standalone app and as part of a multi-app workspace.

## Start from Chat {#start-from-chat}

Use the Chat template when you want a minimal app with the framework wiring already in place:

```bash
npx @agent-native/core@latest create my-template --template chat --standalone
```

For a workspace with multiple apps, run the picker and include Chat with any domain templates you want:

```bash
npx @agent-native/core@latest create my-platform
```

Chat gives you auth, durable chat threads, SQL-backed resources, tools, application state, actions, and polling sync. You add the domain model and product UI.

If you are not building a reusable UI template yet, use the headless on-ramp in [Getting Started](/docs/getting-started#create-your-app): define one action, run it with `pnpm agent`, and add UI later when the workflow needs a durable surface.

## Project Structure {#project-structure}

Every template follows the same broad layout:

<FileTree
  id="doc-block-1pqnp29"
  title="Template project layout"
  entries={[
    {
      path: "app/",
      note: "React frontend",
    },
    {
      path: "app/root.tsx",
      note: "HTML shell and providers",
    },
    {
      path: "app/routes/",
      note: "React Router file routes",
    },
    {
      path: "app/components/",
      note: "Template UI",
    },
    {
      path: "app/hooks/",
      note: "UI state and data hooks",
    },
    {
      path: "actions/",
      note: "defineAction operations — the single source of truth",
    },
    {
      path: "server/db/schema.ts",
      note: "Drizzle schema",
    },
    {
      path: "server/plugins/db.ts",
      note: "additive migrations",
    },
    {
      path: "server/plugins/",
      note: "startup integrations",
    },
    {
      path: "server/routes/api/",
      note: "custom routes only when actions are not enough",
    },
    {
      path: "shared/types.ts",
      note: "shared client/server types",
    },
    {
      path: ".agents/skills/",
      note: "<skill>/SKILL.md — agent guidance for complex workflows",
    },
    {
      path: "AGENTS.md",
      note: "template-specific agent instructions",
    },
    {
      path: "package.json",
    },
    {
      path: "react-router.config.ts",
    },
    {
      path: "vite.config.ts",
    },
  ]}
/>

Do not add a `data/` directory for application state. Durable app data belongs in SQL, and the UI reads it through actions or typed server handlers.

The four areas of every template wire together through one shared action surface and one SQL database — the agent and the UI are equal partners over the same operations:

<Diagram id="doc-block-1rbf8wn" title={"How a template's four areas connect"} summary={"The UI and the agent both reach SQL through the same actions; application state and polling sync keep them aligned."}>

```html
<div class="diagram-tmpl">
  <div class="diagram-col">
    <div class="diagram-node">
      React UI<br /><small class="diagram-muted">app/routes · components</small>
    </div>
    <div class="diagram-node">
      Agent<br /><small class="diagram-muted">AGENTS.md · skills</small>
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center">
    <span class="diagram-pill accent">Actions</span
    ><small class="diagram-muted">defineAction()</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    SQL via Drizzle<br /><small class="diagram-muted">additive schema</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&#8635;</div>
  <div class="diagram-pill ok">Polling sync</div>
</div>
```

```css
.diagram-tmpl {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-tmpl .diagram-col {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.diagram-tmpl .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
.diagram-tmpl .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
```

</Diagram>

## Model Data In SQL {#data-models}

Define domain tables with the framework Drizzle helpers so schemas stay portable across SQLite, Postgres, D1, Turso, Supabase, Neon, and other supported backends:

```ts filename="server/db/schema.ts"
import {
  table,
  text,
  integer,
  now,
  ownableColumns,
  createSharesTable,
} from "@agent-native/core/db/schema";

export const projects = table("projects", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  status: text("status", {
    enum: ["draft", "active", "archived"],
  })
    .notNull()
    .default("draft"),
  sortOrder: integer("sort_order").notNull().default(0),
  ...ownableColumns(),
  createdAt: text("created_at").notNull().default(now()),
  updatedAt: text("updated_at").notNull().default(now()),
});

export const projectShares = createSharesTable("project_shares");
```

Schema changes must be additive. Add tables and columns through `runMigrations()` in `server/plugins/db.ts`; never use destructive SQL, `drizzle-kit push`, table renames, or column drops.

For app reads and writes, use Drizzle's query builder and portable operators from `drizzle-orm`. Do not write product code with raw SQL when Drizzle can express the query, and do not import from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core` in templates.

```ts filename="server/plugins/db.ts"
import { runMigrations } from "@agent-native/core/db";

export default runMigrations(
  [
    {
      version: 1,
      sql: `CREATE TABLE IF NOT EXISTS projects (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'draft',
      sort_order INTEGER NOT NULL DEFAULT 0,
      owner_email TEXT NOT NULL,
      org_id TEXT,
      visibility TEXT NOT NULL DEFAULT 'private',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    )`,
    },
  ],
  { table: "my_app_migrations" },
);
```

Use the [Database](/docs/database) and [Security](/docs/security) docs before adding schemas that hold user or org data.

## Define Operations As Actions {#actions}

Actions are the single source of truth for app behavior. The agent calls them as tools, the frontend calls them through hooks, and other apps can reach them through MCP/A2A.

<AnnotatedCode
  id="doc-block-1o0ohdj"
  title="actions/create-project.ts"
  filename="actions/create-project.ts"
  language="ts"
  code={
    'import { defineAction } from "@agent-native/core/action";\nimport { getDb } from "../server/db/index.js";\nimport { nanoid } from "nanoid";\nimport { z } from "zod";\nimport * as schema from "../server/db/schema";\n\nexport default defineAction({\n  description: "Create a project.",\n  schema: z.object({\n    title: z.string().min(1).describe("Project title"),\n  }),\n  run: async ({ title }, ctx) => {\n    const db = getDb();\n    const id = nanoid();\n    await db.insert(schema.projects).values({\n      id,\n      title,\n      ownerEmail: ctx.userEmail,\n      orgId: ctx.orgId,\n    });\n    return { id, title };\n  },\n});'
  }
  annotations={[
    {
      lines: "2",
      note: "`getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`.",
    },
    {
      lines: "8",
      label: "Tool surface",
      note: "The `description` is what the agent reads to decide when to call this action as a tool.",
    },
    {
      lines: "9-11",
      label: "Typed contract",
      note: "One zod `schema` validates input from the agent, the UI, HTTP, MCP, and A2A.",
    },
    {
      lines: "18-19",
      label: "Scoped write",
      note: "Stamp `ownerEmail` / `orgId` from `ctx` so the row is correctly scoped for sharing and access checks.",
    },
  ]}
/>

Use `http: { method: "GET" }` or `readOnly: true` for read-only actions. Use `parallelSafe: true` only for mutating actions that are safe to run concurrently with same-turn tool calls. Use `toolCallable: false` for high-blast-radius actions that should not run from sandboxed tools.

## Build The UI {#ui}

Routes live in `app/routes/` and use React Router v8 file routing. Query data through actions or API handlers, and make mutations optimistic by default.

```tsx
import {
  useActionMutation,
  useActionQuery,
} from "@agent-native/core/client/hooks";
export default function ProjectsPage() {
  const { data: projects = [] } = useActionQuery("list-projects", {});
  const create = useActionMutation("create-project");

  return (
    <button onClick={() => create.mutate({ title: "Launch plan" })}>
      New project ({projects.length})
    </button>
  );
}
```

Wire live sync once near the app shell so React Query caches refresh when the agent, another tab, or an action changes data:

```tsx
import { useDbSync } from "@agent-native/core/client/hooks";
import { useQueryClient } from "@tanstack/react-query";

export function AppSync() {
  const queryClient = useQueryClient();
  useDbSync({ queryClient });
  return null;
}
```

**The agent-native promise: agent writes show up in the UI without a manual refresh.** `useActionQuery` is the easy path — every hook refetches when a mutating action emits `source: "action"`. If you reach for raw `useQuery` with a custom key (for example, a low-level client helper that reads integration status), fold the per-source counter into the queryKey for targeted refreshes:

```tsx
import { useChangeVersions } from "@agent-native/core/client/hooks";

const v = useChangeVersions(["dashboards", "action"]);
useQuery({
  queryKey: ["dashboard", id, v],
  queryFn: () => fetchDashboard(id),
  placeholderData: (prev) => prev, // no flicker on refetch
});
```

Common sources: `"action"` (every successful agent action — the reliable fallback), `"app-state"`, `"settings"`, plus any custom resource source your store emits via `recordChange`. See the `real-time-sync` skill for the full pattern.

Templates render with the default shadcn-style UI. If your company already has
its own React design system, see [Custom Design Systems](/docs/custom-design-system)
to register it as a typed adapter instead of adopting the default primitives.

## Add Application State {#application-state}

Application state is how the agent knows what the user is seeing. At minimum, add:

- A UI hook that writes semantic `navigation` state when routes, selected records, active tabs, or editor selections change.
- A `view-screen` action that reads that state and returns the current screen snapshot.
- A `navigate` action that writes a one-shot `navigate` command for the UI to consume.

Use `useAgentRouteState` for the UI hook so application-state writes, tab-scoped command reads, delete-after-read, and duplicate-command protection stay consistent:

```tsx
import { useAgentRouteState } from "@agent-native/core/client/navigation";
import { TAB_ID } from "@/lib/tab-id";

export function useNavigationState() {
  useAgentRouteState({
    browserTabId: TAB_ID,
    requestSource: TAB_ID,
    getNavigationState: ({ pathname, searchParams }) => ({
      view: pathname === "/" ? "home" : pathname.slice(1),
      selectedId: searchParams.get("id"),
    }),
    getCommandPath: (command: any) => command.path ?? "/",
    navigateOptions: { replace: true, flushSync: true },
  });
}
```

Keep shareable filters in URL query params. The framework exposes them to the agent as `<current-url>` and the built-in agent can change them with `set-search-params`; `navigation` should hold semantic IDs and aliases, not a second copy of the full query string.

For app navigation, prefer one `navigate` command that includes a same-origin
`path` when the URL is known. Do not also write `__set_url__` for the same move;
that key is reserved for the framework URL tools and URL-only filter changes.

```ts filename="actions/navigate.ts"
import { defineAction } from "@agent-native/core/action";
import { writeAppState } from "@agent-native/core/application-state";
import { z } from "zod";

export default defineAction({
  description: "Navigate the UI.",
  schema: z.object({
    view: z.enum(["home", "project"]),
    projectId: z.string().optional(),
    path: z.string().optional(),
  }),
  run: async (args) => {
    await writeAppState("navigate", args);
    return { ok: true };
  },
});
```

See [Context Awareness](/docs/context-awareness) for the full pattern.

## Use API Routes Sparingly {#api-routes}

Prefer actions for app operations. Create custom Nitro routes only for surfaces that cannot be actions cleanly:

- File upload or binary streaming.
- Public anonymous pages and webhooks.
- OAuth callbacks and provider-specific protocol handlers.
- Server-rendered public content.

Custom routes that touch ownable data must call `getSession(event)` and wrap database work in `runWithRequestContext({ userEmail, orgId }, fn)` before using access helpers.

## Write Agent Instructions {#write-agents-md}

`AGENTS.md` is the agent's map of your app — a small, skimmable file with a
purpose line, core rules, application-state keys, an action table, and a skills
index:

```markdown
# My Template

One workspace for projects, tasks, and notes.

## Core Rules

- Data lives in SQL via Drizzle. Use actions for all writes; schema is additive.
- Use `view-screen` before acting on "this project" if the screen is unclear.

## Application State

- `navigation.view`: `home` | `project`
- `navigation.projectId`: selected project on a project page

## Actions

| Action           | Purpose                  |
| ---------------- | ------------------------ |
| `list-projects`  | List accessible projects |
| `create-project` | Create a project         |
```

Update `AGENTS.md` whenever you add a new action, route, state key, or recurring
workflow. [Writing Agent Instructions](/docs/writing-agent-instructions) is the
full guide — how to keep `AGENTS.md` skimmable, what belongs in each of the four
guidance surfaces, and how to word skill and tool descriptions so the agent
triggers them reliably.

## Add Skills {#skills}

Use skills for detailed patterns that would bloat `AGENTS.md`: provider-specific APIs, import/export formats, complex editing flows, or domain terminology.

```markdown
---
name: project-imports
description: How to import projects from the legacy CSV export.
---

# Project Imports

Use this skill when the user uploads a legacy project CSV.

## Rules

- Validate required columns before creating rows.
- Use `create-project` for each project so ownership and sync are correct.
- Save rejected rows as a note attached to the import summary.
```

Store template skills in `.agents/skills/<name>/SKILL.md`. If users should be able to edit the guidance at runtime, surface it through workspace resources as well.

## Register Setup Steps {#onboarding}

If a template needs an API key, OAuth connection, or provider account, register an onboarding step instead of burying the requirement in a README.

```ts filename="server/plugins/onboarding.ts"
import { defineNitroPlugin } from "@agent-native/core/server";
import { registerOnboardingStep } from "@agent-native/core/onboarding";

export default defineNitroPlugin(() => {
  registerOnboardingStep({
    id: "github",
    title: "Connect GitHub",
    description: "Needed to import repositories and pull requests.",
    order: 100,
    methods: [
      {
        id: "token",
        kind: "form",
        primary: true,
        label: "Save token",
        payload: {
          fields: [
            { key: "GITHUB_TOKEN", label: "GitHub token", secret: true },
          ],
        },
      },
    ],
    isComplete: () => !!process.env.GITHUB_TOKEN,
  });
});
```

See [Onboarding & API Keys](/docs/onboarding).

## Make It Workspace-Ready {#workspace-ready}

Templates should fit naturally into [Multi-App Workspaces](/docs/multi-app-workspace), usually coordinated by [Dispatch](/docs/dispatch).

Checklist:

- Mount A2A through the framework agent chat plugin or `mountA2A()` so sibling apps can call your agent.
- Keep the agent card descriptions specific enough for Dispatch to route work accurately.
- Register required secrets/onboarding so setup appears in the sidebar and Dispatch can manage shared credentials.
- Keep cross-cutting instructions in workspace `AGENTS.md` or workspace resources, not copied into every app.
- Use sharing/access helpers for all ownable resources so org-scoped workspaces stay isolated.

## Publish A Template {#publishing}

Before sharing:

1. Run `pnpm install`, `pnpm typecheck`, and the template's tests.
2. Verify it works with no optional provider keys configured.
3. Check auth, sharing, and two-user data isolation.
4. Document required env vars and onboarding steps.
5. Include examples or seed rows through additive migrations, not tracked runtime data files.

Community templates can be created from a GitHub repo:

```bash
npx @agent-native/core@latest create my-app --template github:user/repo
```

## Contributing to the framework monorepo {#contributing}

### Test unpublished framework changes {#test-unpublished-framework-changes}

When you are working inside the framework monorepo and need a generated
workspace to use unpublished package or template changes, run create with the
local-package flag:

```bash
AGENT_NATIVE_CREATE_USE_LOCAL_CORE=1 pnpm --filter @agent-native/core create my-platform
```

The generated workspace links the local `@agent-native/core` and
`@agent-native/dispatch` packages, so changes to Core APIs, Dispatch workspace
behavior, or first-party templates can be tested before publishing. The package
`prepack` scripts build `dist` before linking, which keeps the generated
workspace pointed at current build output.

## What's next

- [**Writing Agent Instructions**](/docs/writing-agent-instructions) — how to
  keep `AGENTS.md` and skills skimmable as the template grows.
- [**Agent-Native Toolkit**](/docs/agent-native-toolkit) — reusable sharing,
  settings, history, and review surfaces instead of rebuilding them per app.
- [**Custom Design Systems**](/docs/custom-design-system) — render the UI with
  your company's own React components instead of the default shadcn-style UI.
- [**Multi-App Workspace**](/docs/multi-app-workspace) — make the template fit
  naturally alongside sibling apps and Dispatch.
- [**Syncing Template Changes**](/docs/syncing-template-changes) — pull later
  upstream template fixes into an app that was already created from it.
- [**Templates**](/docs/cloneable-saas) — turn a template into a
  publishable product you can share and deploy.
