Put your app in any AI. Register actions where they already live, no refactoring needed.
Want to skip setup? Start from the ready-to-run examples/express-react app and test the full round-trip in minutes.
Agentable is currently experimental. APIs and behavior may change between minor versions while we iterate quickly.
AI agents that can "use your app" usually force you to build and maintain a separate tool layer. Agentable flips that model: register actions directly inside your React components, right next to the state they control. The agent only needs two tools, discoverActions and callAction.
- React-native action registration with direct state and closure access.
- Minimal agent interface:
discoverActions+callAction. - Built-in server bridge for Express and Next.js App Router.
- Optional user confirmation flow for destructive actions.
- Vercel AI SDK adapter plus framework-agnostic
dispatch()usage.
# npm
npm install @vibeflowai/agentable-core @vibeflowai/agentable-react @vibeflowai/agentable-server zod
# pnpm
pnpm add @vibeflowai/agentable-core @vibeflowai/agentable-react @vibeflowai/agentable-server zod AI Agent
│
│ discoverActions() → GET /agentable/manifest
│ callAction(name, params) → GET /agentable/pending (frontend polls)
│ ← POST /agentable/result (frontend posts back)
▼
@vibeflowai/agentable-server ←────────────────────────────────────────────────────────────┐
(your backend) │
@vibeflowai/agentable-react │
(AgentableProvider) → useRegisterAction │
polls /pending (inside your components) │
posts /result ──────────────────────────────────→─┘
AgentableProvidermounts in your React app and POSTs an action manifest to the server.- Your backend agent calls
agentable.dispatch('action.name', params)to queue an action call. - The React provider polls
GET /agentable/pending, executes the matching handler in your component context, then POSTs the result. dispatch()resolves with that handler result.
Express
import express from 'express'
import { createAgentable, expressHandler } from '@vibeflowai/agentable-server'
const app = express()
app.use(express.json())
export const agentable = createAgentable()
app.use('/agentable', expressHandler(agentable))
app.listen(3001)Next.js App Router (app/agentable/[...path]/route.ts)
import { createAgentable, nextHandler } from '@vibeflowai/agentable-server'
export const agentable = createAgentable()
const handle = nextHandler(agentable)
export const GET = handle
export const POST = handle
export const OPTIONS = handleimport { AgentableProvider } from '@vibeflowai/agentable-react'
export default function App() {
return (
<AgentableProvider endpoint="http://localhost:3001/agentable">
<YourApp />
</AgentableProvider>
)
}import { useRegisterAction } from '@vibeflowai/agentable-react'
import { z } from 'zod'
function Counter() {
const [count, setCount] = useState(0)
useRegisterAction(
{
name: 'counter.increment',
description: 'Increment the counter by a given amount.',
schema: z.object({ amount: z.number().int().min(1).default(1) }),
handler: async ({ amount }) => {
const newCount = count + amount
setCount(newCount)
return { newCount }
},
},
[count]
)
return <div>Count: {count}</div>
}The agent can now discover and call counter.increment.
Vercel AI SDK
npm install @vibeflowai/agentable-adapter-vercel-aiimport { toVercelAITools } from '@vibeflowai/agentable-adapter-vercel-ai'
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
const tools = toVercelAITools(agentable)
const result = await streamText({
model: openai('gpt-4o'),
tools,
messages,
})Any other framework
// Inside your tool implementation
const result = await agentable.dispatch('counter.increment', { amount: 5 })
// resolves when the frontend executes the actionMark destructive or irreversible actions with requiresConfirmation: true. dispatch() blocks until the user approves or rejects.
import { useRegisterAction, ConfirmationDialog } from '@vibeflowai/agentable-react'
function DataTable() {
const [rows, setRows] = useState(initialRows)
useRegisterAction(
{
name: 'table.deleteAll',
description: 'Delete all rows from the table.',
schema: z.object({}),
requiresConfirmation: true,
handler: async () => {
setRows([])
return { deleted: true }
},
},
[]
)
return (
<>
<table>{/* ... */}</table>
<ConfirmationDialog
render={({ actionName, params, onApprove, onReject }) => (
<div className="dialog">
<p>
Allow AI to run <strong>{actionName}</strong>?
</p>
{Object.keys(params).length > 0 && (
<pre>{JSON.stringify(params, null, 2)}</pre>
)}
<button onClick={onApprove}>Allow</button>
<button onClick={onReject}>Deny</button>
</div>
)}
/>
</>
)
}If the user denies, dispatch() resolves with { status: 'rejected' }.
If approved, it resolves with { status: 'success', result: ... }.
createAgentable({
timeoutMs?: number // default: 30_000
})Returns an AgentableServer with:
| Method | Description |
|---|---|
handler(req) |
Web API request handler for framework adapters |
dispatch(name, params) |
Queue an action and await the frontend result |
getManifest() |
Get current action manifest, or null before frontend connect |
app.use('/agentable', expressHandler(agentable))const handle = nextHandler(agentable)
export const GET = handle
export const POST = handle
export const OPTIONS = handle<AgentableProvider
endpoint="http://localhost:3001/agentable" // required
pollInterval={500} // optional, default 500ms
context={{ userId, sessionId }} // optional, passed to every handler
>useRegisterAction({
name: string
description: string
schema: ZodObject
requiresConfirmation?: boolean
handler: (params) => Promise<unknown>
}, deps?)Actions auto-unregister on unmount or dependency changes.
const {
registry,
pendingConfirmation, // { callId, name, params } | null
resolvePending, // (callId, approved: boolean) => void
executeAction, // (callId, name, params) => Promise<void>
} = useAgentable()Must be called inside <AgentableProvider>.
render receives:
{
actionName: string
params: unknown
onApprove: () => void
onReject: () => void
}Framework-agnostic primitives used internally by @vibeflowai/agentable-react and @vibeflowai/agentable-server.
const registry = new ActionRegistry()
registry.register(opts)
registry.get(name)
registry.has(name)
registry.size
registry.getManifest()
registry.onChange(listener)const result = await callAction(registry, {
name: string
params: unknown
context?: ActionContext
requestConfirmation?: (name, params) => Promise<boolean>
})
// { status: 'success', result: unknown }
// { status: 'rejected' }
// { status: 'error', message: string }Returns:
discoverActions(no params, returns manifest)callAction({ name, params }, dispatches an action call)
const { discoverActions, callAction } = toVercelAITools(agentable)
const result = await streamText({
model: openai('gpt-4o'),
tools: { discoverActions, callAction },
messages,
})| Package | Description |
|---|---|
@vibeflowai/agentable-core |
Action registry and dispatcher |
@vibeflowai/agentable-react |
React provider, hooks, confirmation dialog |
@vibeflowai/agentable-server |
HTTP bridge for Express and Next.js |
@vibeflowai/agentable-adapter-vercel-ai |
Vercel AI SDK adapter |
examples/express-react: full round-trip with Express, React, and a mock agent.
MIT. See LICENSE.