Reader feedback on documentation piles up faster than anyone can triage it. Someone flags a missing step, the note lands in a queue, and weeks later, the page still has the gap. This guide builds an agent that closes that loop: when a reader leaves feedback on a docs article, the agent reads the article, drafts a fix, stages it as an unpublished draft, and posts a Slack notice with a button to review it in Sanity Studio. A human always approves before anything goes live.
The bot is built on eve, a filesystem-first framework for durable backend agents from Vercel. Sanity Functions trigger it on every new piece of feedback, and a weekly cron runs as a backstop for anything the event path misses. Slack notices are delivered through Vercel Connect, so there is no bot token to manage.
This pattern also applies to other content operations. Anywhere an agent reacts to a trigger and stages a content update for review, the same shape applies to broken-link sweeps, freshness audits, or metadata backfills.
Fork the agent, customize it, and read on to see how it all works.
eve Sanity Docs Agent
Automatically draft documentation fixes in Sanity and notify editors in Slack for review and approval before publishing.
Before you begin, make sure you have:
- Node.js 24+ and a package manager (e.g., pnpm)
- A Vercel account
- Vercel CLI installed (
npm i -g vercel) - A Slack workspace where you can install an app
For Sanity, you’ll also need:
- A Sanity project with a deployed Studio, an
articletype (with a Portable Text body), and afeedbacktype. - A Sanity API token with the Editor role to read content and write drafts.
- Sanity CLI installed (
npm i -g sanity)
The repo is a complete eve agent:
- Tools live in
agent/tools/ - The operating policy in
agent/instructions.md - The model in
agent/agent.ts - The Slack channel and auth in
agent/channels/eve.ts - The Sanity Function trigger under
sanity/
This guide assumes you already have a Sanity project.
If you don't and want to try it anyway, add the Sanity MCP server to your coding agent (e.g., Claude Code), point it at this guide, and ask it to create a project with the content model below and create some example documentation.
The agent works against two document types, article and feedback, which you'll need to add to your Studio's schema.
In a typical layout, that means creating article.ts and feedback.ts under your schema folder (e.g. sanity/schemaTypes/) and registering both in your schema index (e.g. schemaTypes/index.ts).
How these types are used:
- The read tools query them with GROQ.
stage_article_editapplies a single patch to thecontentfield.
If you rename a field or type, update the references to match:
- The GROQ in
agent/tools/read_feedback.tsandagent/tools/read_article.ts - The patch target in
agent/tools/stage_article_edit.ts
Deploy the Studio so the schema is live:
After deploying, grab two values and set them as environment variables:
- Schema ID: get it with
npx sanity schema list, then set it asSANITY_SCHEMA_ID. Agent Actions use this. - Studio App ID:
sanity deploywrites this intosanity.cli.ts. Set it asSANITY_STUDIO_APP_ID, it’ll be used for the Slack review button link.
To send Slack notices, your agent needs Slack bot credentials.
Bundling long-lived API keys into your deployment is risky: the secret sits in your environment indefinitely, applies to every request, and is hard to scope or revoke.
Vercel Connect solves this by issuing short-lived provider tokens at runtime instead. You register a connector for a provider once, link it to your projects and environments, and your code requests a scoped token only when it needs one.
To create a Slack connector, run the following command:
Set the connector UID it prints as the SLACK_CONNECTOR environment variable.
Attach the connector to your Vercel project in the Vercel dashboard, or run:
Invite the agent to the Slack channel and set the channel ID as the SLACK_CHANNEL environment variable. That's the channel the agent posts its review notices to.
Copy the .env.example file, then fill in the values that connect the agent to your Sanity project, Slack, and a model:
Name the token after the agent, for example docs-feedback agent.
Sanity's document history then attributes every staged draft to that identity, so provenance is clear without marking the content itself.
You can create tokens with the Sanity CLI:
AI Gateway and Vercel Connect authenticate requests using Vercel OIDC tokens, which Vercel automatically generates and links to your project.
First, link your local project to a Vercel project:
Then pull your environment variables, which include the OIDC token:
This writes the token to your .env.local file. OIDC tokens are valid for 12 hours, so during local development, you'll need to run vercel env pull again to refresh the token when it expires.
When you deploy to Vercel, OIDC tokens are provisioned automatically, so no further setup is required for production or preview deployments.
Start the dev server:
This opens the eve TUI, where you can drive the agent by hand and watch tool calls.
Option 1: Feed the agent a feedback document directly
In Studio, create a feedback document that references an article. Then give the agent its _id in the TUI:
New reader feedback to handle. Feedback document _id: "<feedback-id>".
You'll see the agent work through its sequence: read_feedback, read_article, stage_article_edit, mark_feedback_handled, and finally post_to_slack. Open the draft from the Slack button to review it.
One note: only the Slack step needs Vercel Connect. Draft staging and feedback-handling work without it, so you can test the core loop even if Slack isn't wired up yet.
Option 2: Test the real trigger path
To exercise the path that runs in production, where the Sanity Function fires the agent, use Sanity's local function testing. Point EVE_AGENT_URL at the address eve dev prints on boot (http://127.0.0.1:2000 by default):
Set the same environment variables in the Vercel project, including a strong secret for the EVE_TRIGGER_SECRET environment variable.
Authentication is already wired up for you.
The verifier in agent/channels/eve.ts handles three cases:
- Accepts requests with
Authorization: Bearer ${EVE_TRIGGER_SECRET} - Falls through to local and Vercel-internal callers
- Returns
401to everyone else
Deploy the Function, then give it the agent's URL and the shared secret. Function environment variables are set per function, after the first deploy:
New feedback now fires the agent. Watch runs with npx sanity functions logs on-feedback, and check each run in eve's dashboard or your Vercel logs.
The agent runs the same short sequence whether an event or the weekly cron starts it:
- Readers leave feedback. A new
feedbackdocument is created in Sanity, referencing the article it's about. - The trigger starts the agent. The Sanity Function fires on create and passes the agent only the feedback
_id. The weekly cron inagent/schedules/weekly-feedback-sweep.tsis the backstop that sweeps up anything the event path missed. - The agent reads the feedback and the article.
read_feedbackloads the comment, andread_articleloads the referenced article's body. - It stages a draft edit.
stage_article_editruns a schema-aware Sanity Agent Action that revises the body and writes the result to a draft. - It marks the feedback handled.
mark_feedback_handledsetshandledAtso a Function retry or the weekly sweep never processes the same item twice. - It posts to Slack.
post_to_slacksends a notice with a review button that opens the draft in Sanity Studio. - Editors review and publish changes. Nothing goes live until a human approves it.
Sanity owns the content and triggers the agent when feedback arrives. eve owns the durable runs, orchestrating which tools the agent can use and where notices go. Because every change lands as a draft and every handled item is marked, the loop is safe to retry and safe to run on a schedule.
Two files carry most of the design: the tool that writes the edit and the guardrails that keep the agent's reach small.
The agent doesn't hand-write Portable Text.
Instead, it composes a precise instruction, and stage_article_edit runs a Sanity Agent Action (transform) that revises the article's body for it. Agent Actions are schema-aware, so the edit is valid content in the right place. By default, they never mutate a published document: pass the published _id and the action writes to the draft, either creating it from the published version or reusing an existing draft.
The exact document gets the edit, the change lands as a draft for a human to review, and the model can't write to the wrong document or publish on its own.
Agent Actions are an experimental API (use apiVersion: "vX").
This agent is triggered by untrusted reader input, so its capability surface is deliberately small. The default eve harness includes shell, filesystem, and network tools (bash, read_file, write_file, glob, grep, web_fetch, web_search).
While they operate in a secure sandbox, you can still disable the ones the agent doesn't need by exporting a sentinel from a file named after each tool:
The agent is then left with five main capabilities, controlled via your own authored tools:
- Read feedback
- Read an article
- Stage a draft
- Mark feedback as handled
- Post the card to Slack.
With no publish or destructive tools, the worst a malicious comment can do is produce a draft for a human to review.
Two more guardrails make the agent more robust:
- The function passes only the feedback
_id, so the agent reads the comment itself rather than trusting a payload. - The
instructions.mdtells the agent to treat the comment as data describing a problem, never as instructions to follow.
- Richer context: Let the agent reason over related content, cheapest first: GROQ references (
*[references($id)]), then keyword ranking withscore()andtext::match, then Sanity Context for schema-aware semantic search. See the comment inagent/tools/read_article.ts. - Fact-checking: Restore the default
web_searchtool so the agent can verify claims on the web before drafting. It's off by default so unverified facts aren’t used. - Batch review: On an Enterprise plan, stage fixes into a Content Release so that a week of fixes is reviewed as a single set.
- More surfaces: Add other eve channels (e.g., Microsoft Teams) for notices, or adopt eve's conversational
slackChannel(the same Slack connector) for interactive buttons and threaded replies.
Symptom: You create a feedback document, but no run appears in eve's dashboard or your Vercel logs.
Cause: The trigger was rejected (a wrong or missing EVE_TRIGGER_SECRET), or the agent was unreachable, or the Function's environment variables aren't set.
Fix: Confirm EVE_AGENT_URL and EVE_TRIGGER_SECRET are set on the Function and that the secret matches the agent's. When a trigger is rejected, the Function throws so Sanity retries rather than failing silently. Inspect the attempts with npx sanity functions logs on-feedback.
Symptom: A Function retry or the weekly sweep re-edits an article you already handled.
Cause: The feedback document wasn't marked handled.
Fix: mark_feedback_handled sets handledAt on each item, and both the event path and the weekly sweep skip anything with handledAt set. Confirm the token in SANITY_API_WRITE_TOKEN has the Editor role so it can write that field.