---
title: "Getting Started"
description: "Create a chat-first agentic app, add an action, render structured results inline, then grow into a persistent page the agent can open."
---

# Getting Started

Agent-Native is for agentic applications: apps where the AI agent and the UI
share the same [actions](/docs/actions), SQL data, and application state. Start
with chat so users can talk to the agent immediately, then add the app surfaces
your workflow earns. New to the concept rather than the code? Read
[What Is Agent-Native?](/docs/what-is-agent-native) first — this page is the
hands-on build path.

The quickest way in is the **Chat template**: a minimal app that gives you a
working AI chat interface, durable threads, auth, and an `actions/` directory
ready to extend. It's the foundation most Agent-Native apps grow from.

The first useful path is:

1. Create a chat app.
2. Connect an AI engine so the agent can respond.
3. Add one action.
4. Render the action result inline in chat.
5. Persist data in SQL.
6. Add a page the agent can open when visual inspection is better than another
   paragraph in the transcript.

Want a complete domain app instead? Clone a rich template such as
[Mail](/docs/template-mail), [Calendar](/docs/template-calendar),
[Forms](/docs/template-forms), [Analytics](/docs/template-analytics), or
[Plan](/docs/template-plan). Want no browser UI yet? See
[Automation-First Apps](/docs/pure-agent-apps) after this tutorial.

## 1. Create a chat app {#create-your-app}

You'll need [Node.js 22+](https://nodejs.org) and [pnpm](https://pnpm.io).

Open a terminal, go to the directory where you want your Agent Native app, and run:

```bash
npx @agent-native/core@latest create my-app --template chat
cd my-app
pnpm install
pnpm dev
```

This gives you durable chat threads, auth, live sync, an `actions/` directory,
standard `view-screen` and `navigate` actions, and a small React app you can
extend.

The dev server starts and opens `http://localhost:8080` in your browser. You may
notice a few startup log lines like `NitroViteError: Vite environment "nitro" is
unavailable`. These are a normal race condition during the initial boot and
resolve on their own. The app is ready once **VITE ready** displays in the
terminal output.

When the app starts, you might find yourself on the login screen rather than in the app. In this case, just sign up with a made up login to satisfy the login screen.
For local development, no email verification is actually required.

> **Want to attach a real debugger?** Run `agent-native dev --inspect` (or
> `--inspect-brk[=<port>]`) instead of `pnpm dev` to attach the Node inspector
> to the dev server, on port `9229` by default. Override the dev runner with
> the `NITRO_DEV_RUNNER` env var if your setup needs a different one.

### If you want a different type of app

This guide uses the Chat template because it is the shortest path
to an agentic application: the agent can act on day one, and you have a real app
surface to grow from. However, if you want to create a different kind of app, run `create` with no flags:

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

The CLI first asks **How do you want to start?** with three choices: **Chat**
(what this guide uses), **Full template(s)**, or **Headless**. Only picking
**Full template(s)** takes you to the domain-template multi-select (Mail,
Calendar, Forms, Analytics, Plan, and more) — Chat and Headless each scaffold a
single standalone app directly, with no further picker.

## 2. Connect an AI engine {#connect-ai}

The agent chat can't respond until you connect an AI engine. After you're logged in, click the **Connect AI** button and pick one of two paths:

| Path                   | Steps                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Connect Builder**    | Click **Connect Builder.io**, choose the Builder Space, then click **Authorize**. No keys to manage yourself.                              |
| **Bring your own key** | Enter your Anthropic or OpenAI API key in the Setup panel. Get an Anthropic key at [console.anthropic.com](https://console.anthropic.com). |

Alternatively, create a `.env` file in the `my-app/` directory (the same
directory that contains `package.json`) before running `pnpm dev`:

```bash
echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env
```

Restart the dev server. Once an AI engine is connected, the Setup panel
hides itself and the agent is ready to chat.

> **Blank screen?** Create a `.env` file in your `my-app/` directory with
> `ANTHROPIC_API_KEY=sk-ant-...`, then restart `pnpm dev`. The in-app Setup
> panel only appears once the app has loaded, so a missing key that prevents
> the app from rendering needs to be fixed via the environment variable rather than
> the UI.

## 3. Add an action {#add-an-action}

An action is a typed operation that both your agent and your UI can call. It's
how the agent does things in your app. Actions live in the `actions/` directory
and can be triggered from chat, from React components, from the CLI, or on a
schedule. You define them once and call them from anywhere.

### Try the starter action

The Chat template includes a `hello` action at `actions/hello.ts`:

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

export default defineAction({
  description: "Return a friendly greeting.",
  schema: z.object({
    name: z.string().default("world").describe("Name to greet"),
  }),
  http: { method: "GET" },
  run: async ({ name }) => {
    return { message: `Hello, ${name}!` };
  },
});
```

Run it from the terminal (inside your `my-app/` directory):

```bash
pnpm action hello --name Alice
```

Or open your app at `http://localhost:8080` and ask the agent in the chat there:

> Use the hello action with the name Alice.

### Add your own action

Replace the starter action with the first real operation in your domain. This example
counts words, sentences, and paragraphs in any text you pass it. It computes
everything locally, so there's nothing to configure and no external service to connect.

Create a new file called `analyze-text.ts` in your `actions/` directory:

```ts filename="actions/analyze-text.ts"
import { defineAction } from "@agent-native/core/action";
import { z } from "zod";

const textStatsSchema = z.object({
  title: z.string(),
  points: z.array(z.object({ label: z.string(), value: z.number() })),
});

export default defineAction({
  description: "Count words, sentences, and paragraphs in a block of text.",
  schema: z.object({
    text: z
      .string()
      .default(
        "The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.",
      ),
  }),
  outputSchema: textStatsSchema,
  chatUI: {
    renderer: "text.stats-chart",
    title: "Text stats",
  },
  readOnly: true,
  run: async ({ text }) => ({
    title: "Text statistics",
    points: [
      { label: "Characters", value: text.length },
      { label: "Words", value: text.split(/\s+/).filter(Boolean).length },
      {
        label: "Sentences",
        value: text.split(/[.!?]+/).filter(Boolean).length,
      },
      {
        label: "Paragraphs",
        value: text.split(/\n\n+/).filter(Boolean).length,
      },
    ],
  }),
});
```

Try it from the terminal:

```bash
pnpm action analyze-text --text "Hello world. How are you today?"
```

Or open your app at `http://localhost:8080` and ask the agent in the chat there:

> Run the analyze-text action on "Hello world. How are you today?"

#### Define once, call from anywhere

This action is now reachable from chat, React hooks, CLI, HTTP, MCP, A2A,
scheduled jobs, and webhooks.

TIP: Any time you want the agent to call a specific action without ambiguity, phrasing it as "Run the `<action-name>` action" is most reliable. Natural-language prompts work well once the agent has enough context about your app's domain. For a brand-new app with no data or context yet, explicit is safer.

## 4. Render the result inline {#render-inline}

When the agent runs `analyze-text`, it returns structured data: a title and an
array of counts. By default the agent will describe that data in prose: "The
text has 9 words, 2 sentences..." and so on. That works, but you can
also render the result as a real UI component (a bar chart, a table, a card)
directly inside the chat transcript, right where the agent responded.

This is what `chatUI.renderer` in the action does. It's a label that says "when
this action's result appears in chat, hand it to this React component instead of
summarizing it in text." The component receives the validated action output as
props and renders whatever you want.

In the next step, you'll create `app/chat-renderers.tsx`, but first, add one import line
to `app/root.tsx` so it runs on startup:

```ts filename="app/root.tsx"
import "./chat-renderers";
```

Add it alongside your other imports at the top of the file. That's the only
change to `root.tsx`. The import just ensures the file runs and registers the
renderer. Now create the renderer file:

```tsx filename="app/chat-renderers.tsx"
import {
  registerActionChatRenderer,
  type ToolRendererProps,
} from "@agent-native/core/client/chat";

type TextStatsResult = {
  title: string;
  points: Array<{ label: string; value: number }>;
};

const MAX_BAR_PX = 80;

function TextStatsChart({ context }: ToolRendererProps) {
  const result = context.resultJson as TextStatsResult;
  const max = Math.max(...result.points.map((point) => point.value), 1);
  return (
    <section className="rounded-lg border bg-card p-4">
      <h3 className="text-sm font-medium">{result.title}</h3>
      <div className="mt-4 flex items-end gap-2">
        {result.points.map((point) => (
          <div
            key={point.label}
            className="flex flex-1 flex-col items-center gap-2"
          >
            <div
              className="w-full rounded-t bg-blue-500"
              style={{
                height: `${Math.max(Math.round((point.value / max) * MAX_BAR_PX), 2)}px`,
              }}
            />
            <span className="text-xs text-muted-foreground">{point.label}</span>
          </div>
        ))}
      </div>
    </section>
  );
}

registerActionChatRenderer({
  id: "text.stats-chart",
  renderer: "text.stats-chart",
  Component: TextStatsChart,
});
```

Once the renderer is registered, the agent's response looks like this. Instead
of a paragraph of text, your React component renders directly inside the chat
transcript:

<WireframeBlock id="doc-block-inline-result-wireframe">
  <Screen
    surface="desktop"
    html={
      "<div style='min-height:340px;box-sizing:border-box;padding:24px;display:flex;justify-content:center;align-items:center;background:var(--wf-bg)'><div style='width:min(640px,100%);display:flex;flex-direction:column;gap:14px'><div class='wf-card' data-rough style='align-self:flex-end;max-width:70%;padding:12px 14px'><strong>User</strong><p style='margin:6px 0 0'>Run the analyze-text action on \"Hello world. How are you today?\"</p></div><div class='wf-card' data-rough style='align-self:flex-start;width:min(480px,100%);padding:14px'><strong>Agent</strong><p class='wf-muted' style='margin:6px 0 12px'>Rendered with text.stats-chart.</p><section class='wf-card' data-rough style='padding:14px'><h3 style='margin:0 0 12px;font-size:14px'>Text statistics</h3><div data-rough='line:bottom' style='height:104px;display:flex;align-items:end;gap:8px;border-bottom:1.4px solid var(--wf-line);padding-bottom:4px'><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:80px;width:100%;background:color-mix(in srgb, var(--wf-accent) 36%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Characters</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:18px;width:100%;background:color-mix(in srgb, var(--wf-accent) 30%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Words</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Sentences</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Paragraphs</span></div></div></section></div></div></div>"
    }
  />
</WireframeBlock>

Use this step when the result belongs where the agent is speaking:

- setup summaries
- short reports
- approvals
- tables or charts small enough to inspect inline
- links into durable app views

For reusable generic outputs, the framework also ships built-in
`data-chart` and `data-table` renderers, plus `data-insights` for combined
summary/chart/table cards. See [Native Chat UI](/docs/native-chat-ui). For
temporary controls the agent creates at runtime, see
[Generative UI](/docs/generative-ui).

## 5. Persist data in SQL {#persist-data}

Right now, every time the agent runs `analyze-text` the result appears in chat
and then disappears. There's nothing to look back at, nothing the agent can
reference later, and no way to build a page around the data. Persisting to SQL
fixes that: the agent writes results to a table, and both the agent and your UI
can read them back at any time.

Agent-Native apps have a SQL database available by default: SQLite locally,
and your configured provider (Postgres, Turso/libSQL, Cloudflare D1) in
production.

### Wire up the database plugin

The Chat template doesn't include a database plugin by default. Create
`server/plugins/db.ts` to initialize it. This is what runs migrations and
makes the database available to your actions:

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

export default runMigrations(
  [
    {
      version: 1,
      sql: `CREATE TABLE IF NOT EXISTS text_analyses (
        id TEXT PRIMARY KEY,
        input TEXT NOT NULL,
        char_count INTEGER NOT NULL,
        word_count INTEGER NOT NULL,
        sentence_count INTEGER NOT NULL,
        created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`,
    },
  ],
  { table: "text_analyses_migrations" },
);
```

Each entry in the array is an additive migration. When you add new columns or
tables later, append a new version object. Never edit existing ones.

### Define the schema

Create `server/db/schema.ts`. The `server/db/` directory may not exist yet,
so create it if needed. This file describes your tables using typed helpers so
your actions get full TypeScript autocomplete:

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

export const textAnalyses = table("text_analyses", {
  id: text("id").primaryKey(),
  input: text("input").notNull(),
  charCount: integer("char_count").notNull(),
  wordCount: integer("word_count").notNull(),
  sentenceCount: integer("sentence_count").notNull(),
  createdAt: text("created_at").notNull().default(now()),
});
```

Use the framework schema helpers (`table`, `text`, `integer`, `now`) rather than
`sqliteTable`, `pgTable`, or dialect-specific imports. They pick the configured
SQL backend automatically, so the same schema runs locally on SQLite and in
production on any supported provider.

After adding both files, restart the dev server so the migration runs:

```bash
pnpm dev
```

Look for these two lines in the terminal output. They confirm the table was created:

```
[db] Applying 1 migration(s) on SQLite/libsql…
[db] Applied migration v1 (1 statement)
```

The `NitroViteError` lines, `BETTER_AUTH_SECRET` warning, and
`SECRETS_ENCRYPTION_KEY` warning that also appear are normal for local dev and
can be ignored.

### Add actions for the table

Now create the action files that read and write the table. These go in your
`actions/` directory, the same place as `hello.ts` and `analyze-text.ts`. You
create them yourself, one file per operation. The agent and your UI will call
them the same way they call any other action.

**`actions/save-text-analysis.ts`** writes a result row to the database.
Call this after running `analyze-text` to make the result durable:

```ts filename="actions/save-text-analysis.ts"
import { defineAction } from "@agent-native/core/action";
import { getDbExec } from "@agent-native/core/db";
import { z } from "zod";

export default defineAction({
  description: "Save a text analysis result to the database.",
  schema: z.object({
    input: z.string(),
    charCount: z.number(),
    wordCount: z.number(),
    sentenceCount: z.number(),
  }),
  run: async ({ input, charCount, wordCount, sentenceCount }) => {
    const id = crypto.randomUUID();
    await getDbExec().execute({
      sql: `INSERT INTO text_analyses (id, input, char_count, word_count, sentence_count)
            VALUES (?, ?, ?, ?, ?)`,
      args: [id, input, charCount, wordCount, sentenceCount],
    });
    return { id };
  },
});
```

**`actions/list-text-analyses.ts`** reads all saved results. The agent can
call this to summarize past analyses, and your UI can use it to populate a page:

```ts filename="actions/list-text-analyses.ts"
import { defineAction } from "@agent-native/core/action";
import { getDbExec } from "@agent-native/core/db";
import { z } from "zod";

export default defineAction({
  description: "List all saved text analyses, newest first.",
  schema: z.object({}),
  run: async () => {
    const result = await getDbExec().execute(
      `SELECT id, input, char_count, word_count, sentence_count, created_at
       FROM text_analyses
       ORDER BY created_at DESC`,
    );
    return result.rows;
  },
});
```

**`actions/delete-text-analysis.ts`** removes a row by id:

```ts filename="actions/delete-text-analysis.ts"
import { defineAction } from "@agent-native/core/action";
import { getDbExec } from "@agent-native/core/db";
import { z } from "zod";

export default defineAction({
  description: "Delete a saved text analysis by id.",
  schema: z.object({ id: z.string() }),
  run: async ({ id }) => {
    await getDbExec().execute({
      sql: `DELETE FROM text_analyses WHERE id = ?`,
      args: [id],
    });
    return { deleted: id };
  },
});
```

Once these files are saved the dev server picks them up automatically. No
restart needed. Try listing analyses from the terminal:

```bash
pnpm action list-text-analyses
```

You should see an empty array. The table exists and the action works; there's
just nothing saved yet:

```
[]
```

Data is saved to `data/app.db`, a SQLite file in your project directory that
gets created automatically on first run. In production you'd point
`DATABASE_URL` at a hosted database instead, but locally this file is all you
need.

To save something, first run `analyze-text` to get the counts:

```bash
pnpm action analyze-text --text "Hello world"
```

You'll see output like:

```
{
  title: 'Text statistics',
  points: [
    { label: 'Characters', value: 11 },
    { label: 'Words', value: 2 },
    { label: 'Sentences', value: 1 },
    { label: 'Paragraphs', value: 1 }
  ]
}
```

Then pass those values to `save-text-analysis`:

```bash
pnpm action save-text-analysis \
  --input "Hello world" \
  --charCount 11 \
  --wordCount 2 \
  --sentenceCount 1
```

Now run `list-text-analyses` again and you'll see the saved row:

```bash
pnpm action list-text-analyses
```

Or ask the agent in the chat at `http://localhost:8080` to do both steps at once:

> Run analyze-text on "Hello world", then save the result.

## 6. Add a page the agent can open {#add-a-page}

Chat is great for conversational interaction, but some data is better inspected
in a dedicated UI: a table you can scan, sort, or delete rows from. This step
adds a React route that displays everything saved in `text_analyses`, using the
same `list-text-analyses` and `delete-text-analysis` actions you already wrote.
There's no second data layer. The page is just a view over the same SQL state
the agent reads and writes.

Create the route file at `app/routes/text-analyses.tsx`. Route files in
`app/routes/` are automatically picked up by the framework. The filename
becomes the URL path, so this page will be available at
`http://localhost:8080/text-analyses`.

```tsx filename="app/routes/text-analyses.tsx"
import {
  useActionMutation,
  useActionQuery,
} from "@agent-native/core/client/hooks";

export default function TextAnalysesRoute() {
  const analyses = useActionQuery("list-text-analyses", {});
  const deleteAnalysis = useActionMutation("delete-text-analysis");

  return (
    <main className="mx-auto flex max-w-3xl flex-col gap-6 p-6">
      <header>
        <h1 className="text-2xl font-semibold">Text analyses</h1>
        <p className="text-muted-foreground">
          Results saved by the agent or triggered manually.
        </p>
      </header>
      <section className="flex flex-col gap-3">
        {analyses.data?.length === 0 && (
          <p className="text-muted-foreground">No analyses saved yet.</p>
        )}
        {analyses.data?.map((row: any) => (
          <article
            key={row.id}
            className="flex items-start justify-between rounded-lg border p-4"
          >
            <div className="flex flex-col gap-1">
              <p className="text-sm font-medium">{row.input}</p>
              <p className="text-xs text-muted-foreground">
                {row.word_count} words · {row.char_count} characters ·{" "}
                {row.sentence_count} sentences
              </p>
            </div>
            <button
              className="text-xs text-destructive hover:underline"
              onClick={() => deleteAnalysis.mutate({ id: row.id })}
            >
              Delete
            </button>
          </article>
        ))}
      </section>
    </main>
  );
}
```

`useActionQuery` calls `list-text-analyses` and keeps the result live. If the
agent saves a new row while the page is open, it appears automatically.
`useActionMutation` calls `delete-text-analysis` when the user clicks Delete,
then invalidates the query so the list refreshes.

Open `http://localhost:8080/text-analyses` in your browser. If you saved an
analysis in the previous step you'll see it listed. Then ask the agent in chat:

> Open the text analyses page.

If you get a 404, try restarting your dev server.

The agent calls the `navigate` action (already included in the Chat
template) to send the browser to `/text-analyses`. This is what it looks like
with a few saved rows:

<WireframeBlock id="doc-block-response-insights-page-wireframe">
  <Screen
    surface="desktop"
    html={
      "<main style='min-height:400px;box-sizing:border-box;padding:28px;background:var(--wf-bg)'><div style='max-width:720px;margin:0 auto;display:flex;flex-direction:column;gap:20px'><header><h2 style='margin:0 0 4px;font-size:24px;font-weight:600'>Text analyses</h2><p class='wf-muted' style='margin:0;font-size:14px'>Results saved by the agent or triggered manually.</p></header><section style='display:flex;flex-direction:column;gap:10px'><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Hello world</p><p class='wf-muted' style='margin:0;font-size:12px'>2 words · 11 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>The quick brown fox jumps over the lazy dog.</p><p class='wf-muted' style='margin:0;font-size:12px'>9 words · 44 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Pack my box with five dozen liquor jugs.</p><p class='wf-muted' style='margin:0;font-size:12px'>8 words · 40 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article></section></div></main>"
    }
  />
</WireframeBlock>

## 7. Extend the navigation {#extend-navigation}

The sidebar's links are a plain array in `app/components/layout/Sidebar.tsx`,
not a separate config file. Open it and add an entry for the Text analyses
page next to the existing Chat entry:

```tsx filename="app/components/layout/Sidebar.tsx"
import { IconList, IconMessageCircle } from "@tabler/icons-react";

const navItems = [
  {
    icon: IconMessageCircle,
    labelKey: "navigation.chat",
    href: "/",
    view: "chat",
  },
  {
    icon: IconList,
    labelKey: "navigation.textAnalyses",
    href: "/text-analyses",
    view: "text-analyses",
  },
];
```

`icon` takes an imported Tabler icon component, not a string name. `labelKey`
looks up a string in the i18n catalog (`app/i18n/en-US.ts` and the other
locale files); an unregistered key still renders — it falls back to a
humanized version of the key (`navigation.textAnalyses` becomes "Text
analyses") — but add it to the catalogs if you want the label translated. See
[Internationalization](/docs/internationalization).

Save the file. The dev server picks up the change automatically and the sidebar
updates without a restart.

### Agent navigation

The sidebar link lets users navigate manually. The agent can also open pages on
its own using two built-in actions that ship with the Chat template:

- **`view-screen`** reads the current route and returns a compact summary of
  what the user is looking at.
- **`navigate`** writes a same-origin path to the browser's history.

As you add more pages, keep `navigate` updated so the agent knows what
destinations exist. Document available paths in `AGENTS.md` so the model can
reason about them.

When the app has both a full-page chat route and an app page, use the shared chat
handoff helpers described in [Agent Surfaces](/docs/agent-surfaces#rich-chat):
`AgentChatSurface`, `AgentSidebar`, `useAgentChatHomeHandoff`,
`useAgentChatHomeHandoffLinks`, and `chatViewTransition`. That lets the full
chat slide into the side panel as the page opens, keeping the same thread while
the user inspects durable data.

## Project structure {#project-structure}

```text
my-app/
  actions/         # Agent-callable and UI-callable operations
  app/             # React routes, pages, and chat surfaces
  server/          # Nitro server and SQL schema
  AGENTS.md        # Always-on instructions for the app agent
  .agents/         # Skills the agent loads when relevant
  data/app.db      # Local SQLite state when DATABASE_URL is unset
```

## Want a full analytics starting point? {#analytics-starting-point}

The text-analyses example above is intentionally small so you can see the
framework pieces. If you are building a real analytics product, start from
[Analytics](/docs/template-analytics) instead. It is the robust starting point:
connect your providers, use the existing dashboards and agent actions, then
customize the app from there.

## What's next {#next}

- **[What Is Agent-Native?](/docs/what-is-agent-native)**: the vision and the
  case for building this way.
- **[Key Concepts](/docs/key-concepts)**: the architecture underneath this
  tutorial — SQL, actions, live sync, context awareness.
- **[Actions](/docs/actions)**: schemas, auth, approvals, hooks, and transport.
- **[Native Chat UI](/docs/native-chat-ui)**: render action results as tables,
  charts, and typed cards.
- **[Chat Template](/docs/template-chat)**: the minimal chat-first app you just
  created.
- **[Analytics Template](/docs/template-analytics)**: a robust analytics app
  starting point; connect providers and customize from there.
- **[Context Awareness](/docs/context-awareness)**: `view-screen`, `navigate`,
  route state, and selected objects.
- **[Agent Surfaces](/docs/agent-surfaces)**: chat, inline UI, app pages,
  embedded sidecars, automation, and external agents.
- **[Deployment](/docs/deployment)**: put your app on your own domain.
- **[FAQ](/docs/faq)**: quick answers on cost, hosting, models, and templates.
