Guides
Agentic Models
Your tables become safe agent tools in one definition. REST, services, tools, prompt and dashboard views from a Drizzle table.
Giving an agent access to your data usually means writing routes, tools, and permission checks
for every table, and keeping them in sync. An Agentic Model is a plain Drizzle table plus
defineAgenticModel. From that one definition you get:
- REST routes under
/api/agentic/models/<name>, in the OpenAPI document. - Typed services:
services.models.customer.list(...)in your server code. - Agent and MCP tools such as
customer_listandcustomer_update, with approval rules. - Prompt resources, so the dashboard prompt can find and propose changes to records.
- Automation outcome targets: an agent can write a reviewed field back.
- A dashboard data view with the columns you choose.
Access rules and workspace scope apply to every one of them.
Defining a Model
The starter's customers model shows every part of a definition:
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { defineAgenticModel, timestamps, workspaceColumns } from '@nuxtagentic-pro/server/models'
export const customers = sqliteTable('customers', {
id: integer().primaryKey({ autoIncrement: true }),
...workspaceColumns(),
name: text().notNull(),
email: text().notNull(),
plan: text({ enum: ['free', 'pro'] })
.notNull()
.default('free'),
summary: text(),
...timestamps(),
})
export default defineAgenticModel(customers, {
name: 'customer',
description: 'Paying customers of our product',
scope: 'workspace',
fields: {
email: { description: 'Billing email', pii: true },
summary: { description: 'Agent-written account summary' },
},
access: { list: 'member', get: 'member', create: 'admin', update: 'admin', delete: false },
agent: { tools: ['list', 'get', 'update'], approval: { update: 'always' } },
outcomes: { writable: ['summary'] },
prompt: { label: 'name', searchable: ['name', 'email'] },
ui: { icon: 'i-lucide-users', columns: ['name', 'plan', 'updatedAt'] },
})
In Nuxt, files in server/models are discovered. In Void, pass the model to
defineVoidAgentic({ models: [customers] }). Then add the table to your schema and migrate it:
pnpm db:generate
pnpm db:migrate
The Nuxt starter's drizzle.config.ts already includes server/models/*.ts; the Void starter
re-exports models from db/schema.ts.
Adding Ownership Columns
Scopes need to know who owns a row. Spread the column helpers into your table:
workspaceColumns()addsownerIdandworkspace. Use it forworkspace,workspace-owner, and function scopes.ownerColumns()adds onlyownerId, for theownerscope.timestamps()addscreatedAtandupdatedAt.
id, ownerId, workspace, createdAt, and updatedAt are managed: clients and agents never
set them, and ownerId and workspace never appear in responses.
Model Options
| Option | Description |
|---|---|
name | Singular name used in routes, services, and tool names |
label | Display name in the dashboard |
description | Tells agents and the prompt what the records are |
scope | Which rows an actor can see; see below |
fields | Per column: label, description, hidden, readonly, pii, schema (a Zod type) |
access | Per action, the minimum role: member, admin, owner, or false |
agent | tools: the actions agents may call; approval: always or never per action |
outcomes | writable: fields an automation record outcome may write |
prompt | label: the column that names a record; searchable: columns for search |
ui | icon and columns for the dashboard data view |
Field names are checked against the table at compile time: a typo in fields, outcomes,
prompt, or ui is a TypeScript error.
Choosing Which Rows an Actor Sees
scope decides which rows each signed-in user can read and change:
| Scope | Rows the actor sees |
|---|---|
workspace | Every row in the current workspace (only their own in personal) |
workspace-owner | Their own rows in the current workspace |
owner | Their own rows in any workspace |
(actor) => filter | A portable filter you build from the actor |
Creates set ownerId and workspace from the actor. Updates and deletes check the scope in the
same SQL statement as the write, so a row cannot move out of scope between the check and the
write.
Limiting Actions by Role
Each action in access needs the listed role or higher. A missing action, or false, means the
route, service method, and tool do not exist. The same rule applies to REST, services, agent
tools, MCP, and the prompt.
Giving Agents Model Tools
agent.tools: ['list', 'get', 'update'] registers customer_list, customer_get, and
customer_update. Enable them on an agent by name:
---
tools: ['customer_list', 'customer_get', 'customer_update']
---
approval: { update: 'always' } holds every update for a person, showing the exact field
changes. hidden fields never appear in responses or tool output; pii marks personal data in
the OpenAPI schema and data views. A model with outcomes.writable fields also registers
customer_update, because automation outcomes write through it.
Querying from Server Code
The services give your own routes the same typed, scoped access:
const { models } = await useAgentic(event)
const page = await models.customer.list({ filter: { plan: 'pro' }, pageSize: 20 })
const customer = await models.customer.get(42)
await models.customer.update(42, { summary: 'Renewed for two years.' })
list accepts page, pageSize (up to 100, default 50), search, sortBy, sortDir,
columns, filters with logic (and or or), and the equality shorthand filter, where
null matches empty values. Results are typed from the table. The
services reference lists every method.
Querying from the Browser
useAgenticClient() calls the same REST routes from your pages:
const client = useAgenticClient()
const { data } = await client('/api/agentic/models/customer', { query: { search: 'acme' } })
The client sends the current workspace and forwards the session during SSR.
Writing Back from Automations
With outcomes: { writable: ['summary'] }, an automation whose payload names a record
({ "record": { "model": "customer", "id": 42 } }) can write its result to summary with a
record outcome. See Outcomes.