Adds a conversational AI agent to a Payload instance so users can read and edit content from messaging apps. It uses Chat SDK adapters for platforms and TanStack AI providers such as Anthropic or OpenAI, and runs generated TypeScript in a sandbox against the Payload Local API. It requires Payload 3.37 or newer and Next.js serverExternalPackages configuration.
AI chat agent for Payload CMS - query and manage your content from Telegram, Slack, WhatsApp and more.
Installation
npm i payload-agent17
Stars
0
Forks
0
Open Issues
1 months ago
Last Updated
51
Weekly Downloads
v0.10.0
npm Version
314.8 KB
Unpacked Size
4
Dependencies
Trends appear after 7 days of data. Collecting since Oct 22 — 1 daily snapshot so far.
Trends appear after 7 days of data. Collecting since Oct 22 — 1 daily snapshot so far.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1205700154-root)<a href="https://payloaddirectory.dev/plugins/1205700154-root"><img src="https://payloaddirectory.dev/badge/1205700154-root.svg" alt="Payload Directory"></a>README
AI chat agent for Payload CMS. Telegram, Slack, WhatsApp and more.
Adds a conversational AI agent directly inside your Payload instance. Users query and manage content through chat platforms without leaving their messaging app.
Powered by Chat SDK for multi-platform messaging, TanStack AI for model orchestration, and Code Mode for reliable tool execution - a sandboxed alternative to MCP that runs TypeScript directly against the Payload Local API.
Work in progress. The core agent loop, chat integrations, content operations, rich text, localization, and media uploads work. Not yet implemented:
payload-agent ships the agent core. You bring one AI provider and one or more chat adapters and pass instances of both into the plugin. The example below uses Claude (Anthropic) and Telegram:
pnpm add payload-agent @tanstack/ai-anthropic @chat-adapter/telegram
Prefer GPT? Use @tanstack/ai-openai @tanstack/ai-client instead. For a different chat platform, swap the adapter for any @chat-adapter/* (see Supported Platforms).
// payload.config.ts
import { anthropicText } from "@tanstack/ai-anthropic";
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { payloadAgentPlugin } from "payload-agent";
export default buildConfig({
plugins: [
payloadAgentPlugin({
adapters: {
telegram: createTelegramAdapter(),
},
agent: {
adapter: anthropicText("claude-sonnet-4-6"),
maxTokens: 8192,
systemPrompt: "Be concise. No emojis.",
},
}),
],
});
payload-agent runs Code Mode in an esbuild + isolated-vm sandbox — native,
server-only packages that Next must not bundle. Spread the list it exports into
serverExternalPackages in your next.config. Keeping the names in the package
(not your config) means they stay correct when you upgrade:
// next.config.ts
import { withPayload } from "@payloadcms/next/withPayload";
import { serverExternalPackages } from "payload-agent/next";
const nextConfig = {
serverExternalPackages, // or: [...serverExternalPackages, ...yourOwn]
};
export default withPayload(nextConfig);
withPayload merges this with its own entries, so the order of the wrappers does
not matter. Skip it and the app fails to boot with Unknown module type /
invalid utf-8 sequence errors pointing at esbuild.
The agent's model adapter is any TanStack AI text
provider — you install the provider package and pass its adapter in. Because the
Code Mode sandbox pins @tanstack/ai to an exact version, install a provider on
the matching line so your tree has a single @tanstack/ai:
| Provider | Install | Import |
|---|---|---|
| Anthropic (Claude) | @tanstack/ai-anthropic@^0.15 | import { anthropicText } from "@tanstack/ai-anthropic" |
| OpenAI (GPT) | @tanstack/ai-openai@^0.14 @tanstack/ai-client | import { openaiText } from "@tanstack/ai-openai" |
payloadAgentPlugin({
agent: { adapter: anthropicText("claude-sonnet-4-6") },
// ...
});
payload-agent declares these providers as optional peer dependencies, so your
package manager warns if an installed version falls outside the supported line.
The current line is @tanstack/[email protected], which the latest providers target — so
installing @tanstack/ai-anthropic or @tanstack/ai-openai works without a
version. If a future provider release outpaces payload-agent, the warning tells
you; pin to the line in the table above (see Troubleshooting).
Other TanStack AI providers work the same way: install one on the 0.28 line and
pass its *Text adapter.
Set the required environment variables:
ANTHROPIC_API_KEY — model provider key (OPENAI_API_KEY if you swapped the
provider)TELEGRAM_BOT_TOKEN — bot token from @BotFatherOn a long-running host (local dev, Railway, a VPS, Docker) that is all you need: the Telegram adapter long-polls for updates, so there is no webhook to set up. Start your app and message the bot. A serverless deploy (Vercel, Lambda) needs one extra step — see Webhooks (serverless).
| Action | Description |
|---|---|
getSchema | Discover collections and their field definitions |
find | Query documents with filters, sort, pagination, and field selection |
findByID | Fetch a single document by ID |
create | Create documents |
update | Partial update a document |
delete | Delete a document |
count | Count documents matching a filter |
uploadFile | Upload a file (a chat attachment or a URL) to an upload collection |
The agent runs TypeScript through Code Mode, so it can compose multi-step operations in a single turn (e.g. read several collections, compute across them, then create a summary document). It ships with a built-in, source-verified reference to Payload's query API and field shapes, so it filters with where, controls depth, and writes data in the right shape. Reads default to depth: 0 — relationship and upload fields come back as ids, and the agent raises depth when it needs related data inline.
delete is off by default, and the agent is scoped to safe collections — see Access control.
When your Payload config has localization enabled, the agent becomes
locale-aware automatically — no extra configuration. find, findByID,
create, update, and count accept locale and fallbackLocale, and the
schema marks which fields are localized.
Because the agent is an LLM, it can actually translate rather than just copy content. A request like "translate post 5 into Spanish and German" becomes:
locale: "all" and fallbackLocale: "false"
to see existing values and which locales are missing.update call with locale: "es" etc.Localized fields are written one locale per call with plain values — the plugin rejects a per-locale object on write (a common mistake that would otherwise corrupt the field) with a message the agent can recover from.
When a collection has an upload config, the agent gets a uploadFile tool.
A user can send a photo or file in chat and ask the agent to save it:
attachmentId (the file bytes never enter the Code Mode
sandbox).uploadFile({ collection, attachmentId, data }), and the
plugin fetches the bytes and creates the upload document.create/update.uploadFile can also fetch from a url instead of an attachment.
payloadAgentPlugin({
// Chat platform adapters
adapters: Record<string, Adapter>,
// AI agent
agent: {
adapter: AnyTextAdapter, // required
maxTokens?: number, // default: 4096
maxWritesPerMessage?: number, // default: 50; writes allowed per message
debug?: boolean, // log agent activity
systemPrompt?: string, // appended to default prompt
},
// Access control (see below). Safe defaults apply without it.
access?: AccessControlConfig,
// How richText (Lexical) fields are exchanged with the agent.
// "markdown" (default): agent reads/writes Markdown, converted to and from
// Lexical editor state via Payload's official converters.
// "lexical": agent works with raw Lexical editor state.
richText?: "markdown" | "lexical",
// State adapter backing conversation history, locks, and dedup.
// Defaults to in-memory. Use a persistent adapter in production (see below).
state?: StateAdapter,
// Concurrency strategy for messages on the same thread.
// Default drops a message that arrives mid-reply; "queue" processes in order.
concurrency?: ConcurrencyStrategy | ConcurrencyConfig,
// Disable plugin without removing from config
disabled?: boolean,
})
By default the agent uses safe scoping: it skips Payload's internal (payload-*)
and auth collections, can create and update but not delete, answers anyone who
messages the bot, and only fetches public http(s) URLs. It is also capped at 50
write operations per message (agent.maxWritesPerMessage) so a single request
can't run away, and is instructed to treat the content it reads as data, not as
instructions to follow.
Use the access option to go further:
payloadAgentPlugin({
access: {
// Act as a Payload user, so the agent obeys that user's access control
// and field permissions instead of running unrestricted.
serviceUser: { collection: "users", id: process.env.AGENT_USER_ID },
// Who may talk to the bot. Runs before every message; fails closed.
authorize: (ctx) => allowedIds.has(ctx.userId),
// Restrict collections, or expose one that is denied by default.
collections: { allow: ["posts", "media"] },
// Toggle writes. delete is off unless you enable it.
operations: { delete: true },
// Reply when authorize refuses; null stays silent.
unauthorizedMessage: "You don't have access to this assistant.",
},
});
Without serviceUser the agent keeps full database access (still bounded by
collections and operations) and warns in production — set it to enforce
Payload's own access control.
Conversation history, per-thread locks, and webhook deduplication are stored in the configured state adapter. The default is in-memory, which is fine for local development but does not persist across restarts and does not work across multiple instances - the bot forgets every conversation on restart.
For production, pass a persistent state adapter. Both auto-detect their connection string from the environment:
// Reuse your Payload Postgres database (zero extra infrastructure):
import { createPostgresState } from "@chat-adapter/state-pg";
payloadAgentPlugin({
// ...
state: createPostgresState(), // reads POSTGRES_URL / DATABASE_URL
concurrency: "queue", // don't drop a user's follow-up mid-reply
});
// Or Redis:
import { createRedisState } from "@chat-adapter/state-redis";
payloadAgentPlugin({
// ...
state: createRedisState(), // reads REDIS_URL
});
Install the adapter you choose (@chat-adapter/state-pg or
@chat-adapter/state-redis). When no state is configured in production, the
plugin logs a warning.
The Telegram adapter picks its transport from the runtime:
TELEGRAM_BOT_TOKEN.POST /api/agent/webhooks/<platform>, where <platform> is the key you used
in adapters (telegram here) and /api is Payload's default API route.The adapter verifies inbound webhooks but does not register the URL for you. Point Telegram at your endpoint once, after deploying:
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
--data-urlencode "url=https://<your-domain>/api/agent/webhooks/telegram" \
--data-urlencode "secret_token=$TELEGRAM_WEBHOOK_SECRET_TOKEN"
Set TELEGRAM_WEBHOOK_SECRET_TOKEN to a random string in both the deployment
environment and the call above; the adapter checks it on every request.
Without it, verification is disabled and any caller that finds the URL can
drive the agent — which can write to your database — so treat the secret as
required in production.
Other platforms work the same way: same endpoint path, each with its own bot token and signing secret. See the Chat SDK docs.
Any adapter from Chat SDK works — install the matching @chat-adapter/<platform> package and pass it in adapters:
@chat-adapter/telegram)Keep all your @chat-adapter/* packages on the same release line (currently 4.x) — each pins the chat core exactly, so mixing lines pulls in a second copy. payload-agent declares chat as a peer dependency, so it shares the single copy your adapters resolve rather than adding its own.
pnpm test # unit + integration tests (Vitest)
pnpm test:eval # agent behavior evals against a live model
pnpm test:eval runs the agent loop end-to-end and asserts on the resulting
database state: creating, updating, and translating content, plus the
guardrails (delete disabled, the per-message write cap, and ignoring injected
instructions). It needs a model API key (ANTHROPIC_API_KEY or
OPENAI_API_KEY), and it is kept out of the normal suite, so it never runs (or
spends) unless you invoke it. Set EVAL_MODEL to pick the model (e.g.
claude-haiku-4-5).
Unknown module type or invalid utf-8 sequence errors pointing at esbuild
on boot. Next is bundling the Code Mode sandbox. Add the
serverExternalPackages entries from Next.js config.
Export buildBaseUsage doesn't exist (or toRunErrorRawEvent) from
@tanstack/ai. Your @tanstack/ai-* provider is on a newer line than the
@tanstack/[email protected] payload-agent builds on, so two copies of @tanstack/ai
ended up in your tree. Pin the provider to the matching line —
@tanstack/ai-anthropic@^0.15 or @tanstack/ai-openai@^0.14 — then reinstall.
Adapter type error mentioning two chat copies or a private
_subjectPromise. Your @chat-adapter/* packages are on different release
lines, so chat (which they pin exactly) is installed twice. Put them all on the
same line (e.g. all 4.30.x) and reinstall. payload-agent no longer bundles
chat — it shares the single copy your adapters bring, so you should not need a
pnpm.overrides.chat workaround.
MIT
Topics
payload ai
AI text, image and voice generation inside Payload rich text and upload fields, with bring-your-own-model support
811.7k/wk558v3
payload ai manager
Generate product titles, descriptions, attributes and translations with Claude, ChatGPT, Gemini or Kimi
585/wk1v3
payload ai plugin
AI assistant in the Payload admin with multiple providers, CMS mentions and signed action proposals
5552/wk4v3
payload plugin ai chat
Adds ChatGPT-style chat to Payload collections using OpenAI models
315/wk15v1
payload ai agent
Admin chat assistant that turns natural language into Payload actions through LLM tool calling
529/wk1v3
payload plugin ai completions
Generate field content in the Payload admin with OpenAI completions using configurable per-field prompts
252/wk8v1