Agent Surfaces

A surface is the way users (or other systems) interact with your app: a chat window, a dashboard page, a background job, an API call from another agent. Agent-Native lets you mix and match these without rebuilding your core logic, because every surface runs the same underlying actions. If you're new to Agent-Native, read Key Concepts first.

How surfaces relate to each other

The four main product shapes sit on a spectrum from most interactive to fully headless. What makes them composable is that they share the same actions and SQL-backed app state. Use the framework agent loop when a surface needs an agent. Adding a new surface doesn't mean rewriting what's underneath — you're adding a new way to reach the same operations.

The surface spectrum
Chatcomposer, transcript, tool calls
Inline UItables, charts, cards
App pagedurable screens, SQL data
headlessAutomationjobs, scripts, external agents
same actions · SQL-backed state · agent loop where used

One action surface, four product shapes, each adding UI without changing the operation underneath.

Choose a starting point

Chat is the most common entry point. Apps typically grow inline UI as output gets richer, then add full app pages when users need persistent objects to browse and share. The same actions power the buttons, scheduled jobs, and external agents that come later. Use the Embedded sidecar when adding an agent to a product you already own, or Automation-first for work that runs without a browser. Here's the full picture:

Surface Use it when Start with
Rich chat Users talk to the agent, see tool calls, and keep a thread history. Chat template, <AgentChatSurface>
Native inline UI Action results should render as tables, charts, cards, or approvals in chat. Native Chat UI, chatUI.renderer
Generated inline UI The agent should create temporary or reusable controls inside chat on the fly. Generative UI, render-inline-extension
Full application Users need durable screens, shared data, navigation, and collaboration. Templates, actions, SQL state, context awareness
Embedded sidecar You already have a SaaS app and want an agent beside it with page context. createAgentNativeEmbeddedPlugin(), AgentNativeEmbedded
Automation-first Jobs, scripts, or other agents call the work directly with no browser UI. agent-native create --headless, defineAction, HTTP, CLI, MCP, A2A
Rich chat on your agent You built the agent elsewhere and want Agent-Native's chat UI around it. AgentChatRuntime, <AssistantChat runtime={runtime}>

Rich chat on Agent-Native

Use the built-in chat when the user should talk to the agent, see tool calls, approve work, inspect native results, and keep a durable thread history.

For a full app starting point, use the Chat template:

npx --yes @agent-native/core@latest create my-chat-app --template chat

The simplest full-page chat:

import { AgentChatSurface } from "@agent-native/toolkit/app/chat";

export default function ChatRoute() {
  return <AgentChatSurface mode="page" className="h-screen" />;
}

When an app has both a full-page chat tab and an AgentSidebar, use the same storageKey on both surfaces, enable chatViewTransition, and install the chat-home handoff helpers in the layout. Ordinary in-app links out of the chat page can then morph the full chat into the sidebar while keeping the active thread:

import { AgentChatSurface, AgentSidebar } from "@agent-native/toolkit/app/chat";
import {
  useAgentChatHomeHandoff,
  useAgentChatHomeHandoffLinks,
} from "@agent-native/core/client/agent-chat";
import { useLocation } from "react-router";

function ChatRoute() {
  return (
    <AgentChatSurface mode="page" storageKey="my-app" chatViewTransition />
  );
}

function AppLayout({ children }: { children: React.ReactNode }) {
  const location = useLocation();
  const handoffActive = useAgentChatHomeHandoff({
    storageKey: "my-app",
    activePath: location.pathname,
    enabled: location.pathname !== "/chat",
  });
  useAgentChatHomeHandoffLinks({ storageKey: "my-app", chatPath: "/chat" });

  return (
    <AgentSidebar
      storageKey="my-app"
      chatViewTransition
      openOnChatRunning={handoffActive}
    >
      {children}
    </AgentSidebar>
  );
}

The simplest embedded chat with your own chrome:

import { AssistantChat } from "@agent-native/toolkit/app/chat";

export function ProjectChat({ threadId }: { threadId: string }) {
  return <AssistantChat threadId={threadId} />;
}

Actions can return explicit native widget results so chat output is not just text. Tables, charts, and typed product cards render as first-party React components in the chat, without iframes. See Native Chat UI. When the agent needs arbitrary generated controls instead of a predefined React widget, use Generative UI: it renders sandboxed Alpine/Tailwind UI inline, can read app state and slot context, and can send selected values back to chat.

Request-scoped action surfaces

Use resolveActionSurface when the selected agent or thread must expose only a server-authorized subset of native actions. The callback runs for every interactive chat request after prepareRequest. Returned names form a hard allowlist for that request: omitted actions are absent from provider schemas, the execution registry, plan-mode preloading, and tool-search discovery.

createAgentChatPlugin({
  actions,
  nativeActionsInDev: true,
  resolveActionSurface: async ({ threadId, availableActionNames }) => ({
    allowedActionNames: await loadAllowedActions(
      threadId,
      availableActionNames,
    ),
  }),
});

When the same resolver also handles unrestricted chat, return { mode: "default" }. This preserves the normal initialToolNames filtering and tool-search discovery instead of loading every available schema on the first model request. The explicit mode is preserved across durable continuations.

An empty list exposes no native actions. Unknown names fail the request instead of widening access. Every allowed action is loaded directly on the first model request; include tool-search explicitly only when discovery inside the already-authorized catalog is wanted. The callback scopes interactive agent chat only—it does not change HTTP, MCP, A2A, job, or trigger exposure. Default framework guidance and spawned sub-agents inherit the same surface. Trusted shell tools are automatically reduced to the request-filtered sandbox because an unrestricted shell cannot enforce a hard action boundary.

Native inline UI

Use this when your actions return structured data — a list of records, a chart dataset, a status summary — that should render as a real UI component inside the chat thread rather than a plain text description. You define a chatUI renderer on the action, and Agent-Native renders it as a first-party React component: no iframes, no separate rendering path.

This is the right choice when the output has a clear, reusable shape that you'd design once and use across many agent responses. For controls the agent needs to create dynamically at runtime, see Generated inline UI instead.

See Native Chat UI for the full renderer API, widget library, and BYO agent runtime integration.

Generated inline UI

Use this when the agent needs to create a control that doesn't exist yet as a pre-built widget — a custom form, a picker built around the current context, a one-off calculator. Unlike native widgets, generated UI is composed by the agent at runtime from Alpine.js and Tailwind, runs sandboxed in an iframe, and can send selected values back into the chat thread.

Generated UI can be transient (rendered once and discarded) or saved as a reusable extension that persists for the user.

See Generative UI for the full API, sandbox constraints, and extension persistence model.

Full application

Use the full app path when users need durable objects and workflows: forms, dashboards, calendars, inboxes, editors, documents, assets, or reports.

Full apps add product UI around the same action and agent contract:

  • SQL state

    App data, navigation, settings, and chat history are durable in SQL. The agent reads and writes the same rows the UI does.

  • Context awareness

    The agent knows the current route, selection, and focused object, so "edit this" always means the right thing.

  • Live sync

    Agent changes update the UI through live sync, and UI changes update the agent's context. The framework uses SSE with polling fallback, so users do not need to refresh manually.

  • Deep links

    Action results can open the right app view directly: a chart links to the dashboard, a draft links to the inbox.

  • Native chat widgets

    Tables, charts, cards, approvals, and typed results render as first-party React components inline in chat.

  • Generative UI and extensions

    The agent can create inline controls on the fly, and save reusable mini-apps when a workflow needs to persist.

Start from the Chat template when you want a minimal app around your actions, or from a domain template when you want a complete product shape.

Full-page Manage Agent

Every Agent-Native app eventually needs a place where users can configure their agent: set standing instructions, review what it's done, connect MCP servers, manage automations, and control access. Building that UI from scratch is a lot of work. Agent-Native ships a pre-built full-page component, AgentTabsPage, that covers all of it across twelve tabs.

First-party templates don't mount it: Settings covers the same ground (Agent › Model, Instructions, Memory, Skills, Files, and Sub-agents, plus the app's Automations and MCP server pages), /agent redirects into Settings, and agentPageHref="/settings/agent" on AgentSidebar opens Settings › Agent › Model. Mount AgentTabsPage on its own route when an app wants the whole agent surface on one page.

app/routes/agent.tsx
import { AgentTabsPage } from "@agent-native/toolkit/app/agent-page";
export default function AgentRoute() {
  return <AgentTabsPage />;
}

The shared page currently provides twelve tabs across two groups:

Group Tab Shows
Resources Files The existing ResourcesPanel for personal or organization files
Resources Instructions Always-on AGENTS.md-style rules
Resources Agents Custom sub-agent profiles
Resources Memory Long-term recall notes
Resources Skills Reusable workflows
Resources Learnings Corrections and patterns captured over time
Resources Remote agents A2A connections to other agent-native apps (replaces what the Connections tab used to show)
Agent Snapshots A scope preview, token budget, ordered system sections grouped by provenance/governance/source, and the latest live-thread snapshot. Renamed from "Context"; old #context links redirect here.
Agent Connections MCP server management only
Agent Automations Personal and organization Scheduled/Event automations with pause/resume, details, and delete flows. The stable compatibility URL remains /agent#jobs.
Agent Settings Agent model, API keys, limits, voice, and automation settings
Agent MCP The app MCP URL, an A2A agent card when available, and shared setup guides for Claude, ChatGPT, Cursor, Claude Code, Codex, and other clients. Find it in Settings → MCP server or use /mcp/connect for the full connect flow and token fallback.

The page shows personal (user-scope) data only. There is no org-level toggle today. It is a thin shell over existing components and access checks, not a new admin console: Connections describes what the app can call; MCP describes how external clients connect to it.

Not yet included:

  • Organization-scoped view
  • Grants and scope editing
  • Revocation UI
  • Per-iteration provenance history

Embedded sidecar

Use the embedded sidecar when the main product already exists and you want an agent beside it.

The server plugin mounts Agent-Native routes into your host app and resolves host identity server-side:

import { createAgentNativeEmbeddedPlugin } from "@agent-native/core/server";

export default createAgentNativeEmbeddedPlugin({
  databaseUrl: process.env.AGENT_NATIVE_DATABASE_URL,
  auth: getHostSession,
  actions: hostActions,
});

The React sidecar passes page context and host commands:

import { AgentNativeEmbedded } from "@agent-native/core/client/host";
export function AppShell({ children }) {
  return (
    <AgentNativeEmbedded
      getContext={() => ({
        route: { pathname: window.location.pathname },
        selection: { text: window.getSelection()?.toString() || undefined },
      })}
      onNavigate={(payload) =>
        router.navigate((payload as { path: string }).path)
      }
      onRefresh={() => queryClient.invalidateQueries()}
    >
      {children}
    </AgentNativeEmbedded>
  );
}

How it connects

The two pieces work as a bridge: the host app passes page context (current route, selected text, focused object) into AgentNativeEmbedded, and the agent sends commands back out through onNavigate and onRefresh. The server plugin handles identity — it resolves the host session so the agent acts as the right user without a separate login. Nothing in the host app needs to change; the plugin attaches Agent-Native's routes alongside your existing ones.

How the sidecar bridges to a host app
Host appyour existing SaaS
getContext()
route · selection
onNavigate / onRefresh
host commands
AgentNativeEmbeddedagent + resources
Agent-Native routes
mounted by the server plugin

The plugin mounts Agent-Native routes server-side; the React sidecar streams page context in and host commands out.

See Embedding SDK for host auth, database isolation, iframe/picker mode, and lower-level bridge APIs.

Automation-first app

Use the automation-first path when no one needs a custom browser screen while the work runs: scheduled jobs, integrations, backend workflows, CLI loops, another agent, or an existing product calling into Agent-Native.

This is the shape to reach for when automation is the product surface. You send a request from the terminal, Slack, email, a scheduled job, another agent, or Chat ("summarize my unread emails," "post the daily metrics to Slack," "find the candidates who replied last week") and the agent acts and returns the result wherever it belongs. It is still a real app, not a stateless prompt: actions, auth sessions, app state, thread/run history, settings, credentials, and share records all live in SQL.

Pick this pattern when:

  • The work happens in the background. Most of the value is created while the user isn't looking: triage agents, daily-report agents, on-call responders.
  • The output leaves the app. The agent posts to Slack, sends email, or updates a third-party system; there's nothing to browse in-app.
  • The domain is one-shot. Research bot, summary generator, report writer with no persistent object that needs a list view.
  • You're prototyping an automation. Ship the operation now; add chat or app pages when users need to inspect and steer it.

If your product is built around persistent objects users browse, pivot, and share (emails, events, documents, charts), pick a full application or a template instead; those add a full UI plus the agent.

What ships in the box

An automation-first app skips dashboard work, and it is channel-agnostic from day one. The same agent runs from the web, Slack, Telegram, email, and other agents because everything goes through the same actions. The trade-off is there is no "browse-everything-at-a-glance" view; if users need that, start from Chat or add a small status page or list view.

When you add the built-in Chat shell, the framework provides five management surfaces you don't have to build: Chat (the main input), Resources (skills, memory, instructions, sub-agents, and connected MCP servers), Automations, Thread history, and Settings. Those are usually enough: talk to it, see what it's done, configure how it behaves. Reach for Chat when you're ready to add that browser UI, or the Dispatch template for a workspace-style starting point with Slack/Telegram, scheduled jobs, and shared secrets out of the box.

The smallest no-browser local path is a scaffold plus one action:

npx --yes @agent-native/core@latest create my-agent --headless
cd my-agent
pnpm install

Then define the durable operation:

actions/summarize-week.ts
import { defineAction } from "@agent-native/core/action";
import { z } from "zod";

// One action powers every app surface: UI, agent, HTTP, MCP, A2A, and CLI.
export default defineAction({
  description: "Summarize this week's submissions.",
  readOnly: true,
  schema: z.object({ formId: z.string() }),
  run: async ({ formId }) => {
    return { formId, summary: "34 submissions, up 18% from last week." };
  },
});

One action is then callable as:

  • HTTP: POST /_agent-native/actions/summarize-week
  • CLI: pnpm action summarize-week --formId form_123
  • App-agent CLI: pnpm agent "Summarize form_123"
  • MCP: from Claude, ChatGPT, Codex, Cursor, OpenCode, Copilot, and other MCP hosts
  • A2A: from another agent-native app or agent peer
  • UI: through useActionQuery, useActionMutation, or callAction
  • Agent tool: from the built-in chat loop
Calling an action over HTTP

This is not a no-database or stateless mode. The app-agent loop stores sessions, threads, runs, settings, credentials, application state, and share records in SQL. Local development uses PGlite; hosted automation-first apps should use a persistent PostgreSQL database.

If you need the whole agent loop headlessly from the project folder, use:

pnpm agent "Summarize this week's forms."

If another app or script needs to call the whole agent, use agentNative.invoke("analytics", "...") or the agent-native invoke CLI. That keeps cross-app work on the A2A path while local work stays on actions.

Workers, jobs, integration webhooks, and custom hosts can drive the agent loop directly through the server API. This is lower-level than actions — you provide the engine, model, messages, tools, actions, an event sink, and an abort signal yourself:

import { runAgentLoop } from "@agent-native/core/server";

await runAgentLoop({
  engine,
  model,
  systemPrompt,
  tools,
  actions,
  messages,
  send,
  signal,
});

For most apps, scheduled prompts and integration webhooks already call this loop for you. Reach for it directly only when building a custom no-browser host, eval runner, or server-side orchestration surface. See Server: Production agent handler for the full signature.

Running against a folder

If your goal is "run an agent against this folder," start with the app-agent loop in that folder: scaffold the automation-first app, add actions/instructions, run pnpm agent "...". That keeps the work inside the same action/runtime/state contract the app will use in production.

External coding harnesses are a separate product surface for embedding Claude Code, Codex, Pi, Cursor, Mastra, or similar runtimes inside an Agent-Native app. Use them when you are building a coding-agent product, not as the default way to start a local agent-native workflow.

Cloud repo access

For cloud automation-first apps that need repository access, use the GitHub connector plus token CRUD model: list repositories, search files, read files, create or edit files, delete files, and revoke access through provider-scoped credentials. In local development, set the target repository explicitly:

GITHUB_REPOSITORY=owner/repo pnpm agent "Read README.md and suggest the next action."

Do not treat a VM clone or long-lived sandbox checkout as the primary cloud repo-access model. Sandboxes still matter for isolated code execution, but repository access should be explicit, permissioned, auditable, and revocable through the connector layer.

Sharing sessions and runs

Automation-first sessions and runs are durable objects. Shareability should be phased: read/share links first, so teammates can inspect sanitized prompts, outputs, and run status; permissioned writable collaboration later, so continuing a run, approving actions, editing schedules, or changing configuration goes through explicit access checks.

Rich chat on your agent

Use this path when your agent is already built with another framework or runtime and you want Agent-Native's chat UI around it. AgentChatRuntime is the boundary: your runtime streams normalized events, and Agent-Native renders the composer, transcript, tool calls, approvals, native widgets, and app layout.

import { AssistantChat } from "@agent-native/toolkit/app/chat";
import { createHttpAgentChatRuntime } from "@agent-native/core/client/agent-chat";

const runtime = createHttpAgentChatRuntime({
  endpoint: "/api/support-agent/chat",
});

export function SupportAgentChat() {
  return <AssistantChat runtime={runtime} threadId="support" />;
}

Ready-made runtime helpers exist for OpenAI Agents, OpenAI Responses, the Claude Agent SDK, the Vercel AI SDK, and AG-UI, plus the normalized HTTP runtime above for any other agent (Mastra, Flue, Eve, LangGraph, or a custom service). ACP is not the end-user app chat or A2A transport, and Agent-Native does not currently claim A2UI support. ACP is supported in one specific place: driving a local coding agent (Gemini CLI, Claude Code, …) through the harness layer, not as the chat runtime here.

Native Chat UI: BYO agent runtimes is the canonical home for the event shapes, the runtime helpers, and chatUI tool-result metadata. Start there when wiring an external agent into the chat.