| name | agent-native-framework |
| description | Build agent-native applications with shared actions, SQL-backed state, tools, skills, and UI surfaces that work together. |
| triggers | ["how do I build an agent-native app","create an agent native application with actions","set up agent-native framework with database","define actions that work with UI and agents","add agent skills to my coding assistant","build agents that act inside real apps","integrate agent runtime with SQL state","create shared actions for agents and UI"] |
Agent-Native Framework
Skill by ara.so — AI Agent Skills collection.
Agent-Native is an open-source framework for building robust agents that act inside real apps, not just chat next to them. It provides primitives for product-grade agentic software: shared actions, SQL-backed state, identity, tools, skills, jobs, observability, and UI surfaces.
Installation
Create a New Project
npx @agent-native/core@latest create my-app
cd my-app
pnpm install
pnpm dev
The create command offers three starting points:
- Full template(s): Clone complete apps (Mail, Calendar, Forms, Clips, etc.)
- Chat: Minimal chat UI with browser shell
- Headless: Action-first app with no UI shell
Use flags to skip the prompt:
npx @agent-native/core@latest create my-app --template mail
npx @agent-native/core@latest create my-app --headless
npx @agent-native/core@latest create my-app --standalone
Add Skills to Existing Coding Agent
Install visual planning and PR recap skills:
npx @agent-native/core@latest skills add visual-plan
This adds /visual-plan and /visual-recap slash commands to Claude Code, Cursor, Codex, Pi, OpenCode, GitHub Copilot, and similar agents.
Core Concepts
Actions
Actions are the fundamental building block. Define work once, use it everywhere: UI, agent, HTTP API, MCP, A2A, and CLI.
import { defineAction } from '@agent-native/core';
import { z } from 'zod';
import { db } from '~/db';
import { replies } from '~/db/schema';
export default defineAction({
schema: z.object({
emailId: z.string(),
body: z.string(),
}),
run: async ({ emailId, body }) => {
await db.insert(replies).values({ emailId, body });
},
});
Calling Actions
From UI:
import { useAction } from '@agent-native/react';
import replyToEmail from '~/actions/reply-to-email';
function ReplyButton({ emailId }: { emailId: string }) {
const { execute, loading } = useAction(replyToEmail);
return (
<button
onClick={() => execute({ emailId, body: 'Thanks for reaching out!' })}
disabled={loading}
>
Send Reply
</button>
);
}
From Agent:
From CLI:
pnpm agent-native action reply-to-email --emailId "123" --body "Hello"
From HTTP API:
curl -X POST http://localhost:3000/api/actions/reply-to-email \
-H "Content-Type: application/json" \
-d '{"emailId": "123", "body": "Hello"}'
Agent Configuration
Basic Agent Setup
import { defineAgent } from '@agent-native/core';
export default defineAgent({
name: 'my-assistant',
model: 'gpt-4',
systemPrompt: `You are a helpful assistant that helps users manage their emails.
You have access to actions like reply-to-email, archive-email, and search-emails.`,
tools: [
],
memory: {
enabled: true,
},
});
Agent with Custom Tools
import { defineAgent, defineTool } from '@agent-native/core';
import { z } from 'zod';
const searchWeb = defineTool({
name: 'search-web',
description: 'Search the web for information',
schema: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
const response = await fetch(
`https://api.search.com/search?q=${encodeURIComponent(query)}`,
{
headers: { 'Authorization': `Bearer ${process.env.SEARCH_API_KEY}` }
}
);
return response.json();
},
});
export default defineAgent({
name: 'research-assistant',
model: 'gpt-4-turbo',
tools: [searchWeb],
});
Database Setup
Agent-Native uses Drizzle ORM with any SQL database (PostgreSQL, MySQL, SQLite).
Schema Definition
import { pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
export const emails = pgTable('emails', {
id: serial('id').primaryKey(),
subject: text('subject').notNull(),
body: text('body').notNull(),
from: varchar('from', { length: 255 }).notNull(),
to: varchar('to', { length: 255 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const replies = pgTable('replies', {
id: serial('id').primaryKey(),
emailId: serial('email_id').references(() => emails.id),
body: text('body').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
Database Connection
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error('DATABASE_URL is not set');
}
const client = postgres(connectionString);
export const db = drizzle(client, { schema });
Environment Variables
# .env
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
OPENAI_API_KEY=your-key-here
Advanced Action Patterns
Action with Database Query
import { defineAction } from '@agent-native/core';
import { z } from 'zod';
import { db } from '~/db';
import { emails } from '~/db/schema';
import { eq } from 'drizzle-orm';
export default defineAction({
name: 'get-email',
schema: z.object({
id: z.string(),
}),
run: async ({ id }) => {
const email = await db
.select()
.from(emails)
.where(eq(emails.id, parseInt(id)))
.limit(1);
return email[0] || null;
},
});
Action with Side Effects
import { defineAction } from '@agent-native/core';
import { z } from 'zod';
import { db } from '~/db';
import { emails } from '~/db/schema';
export default defineAction({
name: 'send-email',
schema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
run: async ({ to, subject, body }) => {
const [email] = await db
.insert(emails)
.values({ to, subject, body, from: 'me@example.com' })
.returning();
await fetch('https://api.emailservice.com/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.EMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, subject, body }),
});
return email;
},
});
Action with Validation and Error Handling
import { defineAction } from '@agent-native/core';
import { z } from 'zod';
export default defineAction({
name: 'process-payment',
schema: z.object({
amount: z.number().positive(),
currency: z.enum(['USD', 'EUR', 'GBP']),
customerId: z.string(),
}),
run: async ({ amount, currency, customerId }) => {
try {
const response = await fetch('https://api.payment.com/charge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYMENT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount, currency, customer: customerId }),
});
if (!response.ok) {
throw new Error(`Payment failed: ${response.statusText}`);
}
return response.json();
} catch (error) {
console.error('Payment error:', error);
throw error;
}
},
});
Skills System
Installing Skills
npx @agent-native/core@latest skills add visual-plan
npx @agent-native/core@latest skills list
npx @agent-native/core@latest skills remove visual-plan
Using Visual Plan Skill
After installing visual-plan, use these slash commands in your coding agent:
/visual-plan: Generate a structured plan with diagrams, wireframes, and file-by-file implementation maps before coding
/visual-recap: Generate a visual recap of changes with a shareable review link after PR or git diff
Creating Custom Skills
import { defineSkill } from '@agent-native/core';
export default defineSkill({
name: 'custom-analyzer',
description: 'Analyzes code patterns and suggests improvements',
commands: [
{
name: 'analyze',
description: 'Analyze current file for patterns',
execute: async (context) => {
const currentFile = context.getCurrentFile();
return {
suggestions: ['Use const instead of let', 'Extract this function'],
};
},
},
],
});
React Integration
Setup React App
import { AgentProvider } from '@agent-native/react';
import { Outlet } from '@remix-run/react';
export default function Root() {
return (
<AgentProvider>
<Outlet />
</AgentProvider>
);
}
Using Agent in Components
import { useAgent, useAction } from '@agent-native/react';
import sendEmail from '~/actions/send-email';
function EmailComposer() {
const { chat, messages, isLoading } = useAgent();
const { execute: send } = useAction(sendEmail);
const handleSend = async () => {
await send({
to: 'user@example.com',
subject: 'Hello',
body: 'Message body',
});
};
const askAgent = async (prompt: string) => {
await chat(prompt);
};
return (
<div>
<button onClick={handleSend}>Send Email</button>
<button onClick={() => askAgent('Draft a follow-up email')}>
Ask Agent to Draft
</button>
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
</div>
</div>
);
}
Real-time State Sync
import { useRealtimeState } from '@agent-native/react';
import { emails } from '~/db/schema';
function EmailList() {
const { data: emailList, mutate } = useRealtimeState(emails);
return (
<ul>
{emailList?.map(email => (
<li key={email.id}>{email.subject}</li>
))}
</ul>
);
}
Jobs and Background Tasks
import { defineJob } from '@agent-native/core';
import { z } from 'zod';
export default defineJob({
name: 'process-attachments',
schedule: 'every 5 minutes',
schema: z.object({
emailId: z.string(),
}),
run: async ({ emailId }) => {
console.log(`Processing attachments for email ${emailId}`);
},
});
Triggering Jobs from Actions
import { defineAction, scheduleJob } from '@agent-native/core';
import { z } from 'zod';
import processAttachments from '~/jobs/process-attachments';
export default defineAction({
name: 'receive-email',
schema: z.object({
emailId: z.string(),
}),
run: async ({ emailId }) => {
await scheduleJob(processAttachments, { emailId });
},
});
Observability
Logging
import { defineAction, log } from '@agent-native/core';
export default defineAction({
name: 'complex-action',
schema: z.object({ data: z.string() }),
run: async ({ data }) => {
log.info('Starting complex action', { data });
try {
log.debug('Processing step 1');
log.debug('Processing step 2');
return { success: true };
} catch (error) {
log.error('Action failed', { error, data });
throw error;
}
},
});
Tracing
import { defineAgent, trace } from '@agent-native/core';
export default defineAgent({
name: 'traced-agent',
onMessage: async (message, context) => {
const span = trace.startSpan('agent-message');
try {
span.setAttributes({ messageLength: message.length });
return await context.complete(message);
} finally {
span.end();
}
},
});
MCP (Model Context Protocol) Integration
Agent-Native actions automatically expose as MCP tools:
A2A (Agent-to-Agent) Communication
import { defineAgent, callAgent } from '@agent-native/core';
export default defineAgent({
name: 'coordinator',
onMessage: async (message, context) => {
const result = await callAgent('specialist-agent', {
task: 'analyze',
data: message,
});
return `Coordinated result: ${result}`;
},
});
Deployment
Environment Setup
# Production environment variables
DATABASE_URL=postgresql://prod-user:password@prod-host:5432/prod-db
OPENAI_API_KEY=sk-prod-key
NODE_ENV=production
PORT=3000
Build and Deploy
pnpm build
pnpm start
Agent-Native works with Nitro-compatible hosting providers:
- Vercel
- Netlify
- AWS Lambda
- Cloudflare Workers
- Node.js servers
Troubleshooting
Action Not Found
Problem: Agent can't find defined action.
Solution: Ensure action is exported as default and file is in actions/ directory:
export default defineAction({ });
Database Connection Issues
Problem: DATABASE_URL is not set error.
Solution: Check .env file exists and is loaded:
echo $DATABASE_URL
cp .env.example .env
Agent Not Responding
Problem: Agent doesn't call actions or respond to prompts.
Solution:
- Check API key is set:
OPENAI_API_KEY or relevant model provider key
- Verify system prompt includes action descriptions
- Check action schemas are valid Zod schemas
Type Errors with Actions
Problem: TypeScript errors when using actions.
Solution: Regenerate types after schema changes:
pnpm generate
Real-time State Not Updating
Problem: UI doesn't reflect agent changes.
Solution: Ensure useRealtimeState is used and WebSocket connection is active:
const { data, connected } = useRealtimeState(table);
console.log('Connected:', connected);
Common Patterns
Multi-step Agent Workflow
import { defineAgent } from '@agent-native/core';
import searchEmails from '~/actions/search-emails';
import replyToEmail from '~/actions/reply-to-email';
export default defineAgent({
name: 'email-assistant',
systemPrompt: `You help manage emails. Follow these steps:
1. Search for relevant emails using search-emails
2. Read the content and understand context
3. Draft appropriate replies using reply-to-email`,
tools: [searchEmails, replyToEmail],
});
Shared State Between UI and Agent
export const updateDocument = defineAction({
schema: z.object({ id: z.string(), content: z.string() }),
run: async ({ id, content }) => {
return await db.update(documents)
.set({ content })
.where(eq(documents.id, id))
.returning();
},
});
function DocumentEditor({ id }: { id: string }) {
const { data: doc } = useRealtimeState(documents, eq(documents.id, id));
const { execute: update } = useAction(updateDocument);
return <Editor value={doc?.content} onChange={(val) => update({ id, content: val })} />;
}
Authentication and Authorization
import { defineAction } from '@agent-native/core';
import { z } from 'zod';
export default defineAction({
name: 'delete-email',
schema: z.object({ id: z.string() }),
auth: true,
run: async ({ id }, context) => {
if (!context.user) {
throw new Error('Unauthorized');
}
const email = await db.query.emails.findFirst({
where: eq(emails.id, id),
});
if (email?.userId !== context.user.id) {
throw new Error('Forbidden');
}
await db.delete(emails).where(eq(emails.id, id));
},
});