How I built a self-improving content agent with Eve on Vercel
A guide to building a self-improving content agent with Eve on Vercel: cron schedules, PostHog feedback, and the auth trap that breaks scheduled runs.
The content workflow behind this site runs when I run it. Every stage fires because I opened a terminal and typed a command. That works, and it also means the whole operation stops when my week gets eaten by client work, which is most weeks.
So I built the version that runs without me: a self-improving content agent on Eve, Vercel’s new filesystem-first agent framework. Every Monday morning it reviews content performance in PostHog, researches the last two weeks of AI-in-GTM material, filters what it finds against a positioning file I own, updates an editorial calendar, and writes a full blog draft plus three LinkedIn drafts. Then it stops. A human reviews and publishes, or doesn’t.
It’s deployed to production with a live weekly cron. Here is the full build, including the auth trap that breaks most scheduled-agent designs on the first try.
What a self-improving content agent does every Monday
The loop has eight steps, written as numbered instructions in a markdown file:
- Load the brand voice skill before writing anything.
- Query PostHog for how published content performed, and update a running insights file.
- Research the last 14 days of the web in its beat: analyst POVs, practitioner takes, market news. Three queries minimum, distinct angles.
- Filter findings against
conversations-to-own.md, a positioning file the agent reads but never edits. - Update the editorial calendar: this week’s pick plus a ranked backlog.
- Write the blog post draft, with primary sources cited in frontmatter.
- Write three LinkedIn drafts in set formats: finding-led, contrarian, playbook.
- Report what it researched, what it wrote, and what needs a human decision.
The self-improving part is step 2. PostHog observations accumulate in an insights file, and the agent reads that file before writing anything new. Performance data from published posts shapes the angle and headline choices in future drafts. No vector store and no memory framework, just a markdown file I can read and edit myself.
Nothing publishes automatically. I want a human deciding what ships, so the agent produces drafts plus a report of what needs a decision from me.
Why Eve fits a content system
Eve’s core idea: an agent is a directory of files. Instructions are markdown. Tools are TypeScript files. Schedules are markdown files with cron frontmatter. Skills are markdown procedures loaded on demand.
For a content system this is a natural fit, because the agent’s working memory can be plain files in the same repo, reviewable in git:
agent/
instructions.md # the 8-step loop, in prose
agent.ts # defineAgent({ model: "anthropic/claude-sonnet-5" })
skills/sandsdx-voice.md # brand voice rules, loaded before writing
tools/ # research_web, list_content, read_content, write_content
connections/posthog.ts # PostHog MCP server
schedules/content-loop.md
content/
conversations-to-own.md # HUMAN-OWNED positioning filter
calendar.md # AGENT-OWNED editorial calendar
insights.md # AGENT-OWNED performance memory
posts/ # blog drafts
social/ # LinkedIn drafts
The instructions file is explicit about ownership. The agent maintains the calendar, the insights file, and the drafts. It reads conversations-to-own.md and may only append dated suggestions to one designated section. That single rule is what keeps a research agent on-thesis instead of chasing whatever the web served up that week.
What you need
- Node.js 24 or newer. Eve’s engines check fails hard on 22.
- A Vercel account. The deploy target, the model gateway, and the content store are all Vercel services.
- A PostHog project with content analytics, if you want the feedback loop. The agent degrades gracefully without it.
- About an afternoon.
Scaffold and connect:
npx eve init content-agent --channel-web-nextjs
cd content-agent
vercel link
npm run dev
vercel link drops a VERCEL_OIDC_TOKEN into .env.local, and model calls route through Vercel’s AI Gateway with zero API keys. npm run dev gives you a chat UI at localhost:3000 talking to your agent.
Web research without a search API key
The obvious way to give an agent research ability is a search API: Exa, Firecrawl, Browserbase. All of them want an account and a key. I skipped all of that by calling a search-grounded model through the gateway the project is already authenticated with. In AI SDK v7, a plain "provider/model" string routes through the gateway:
// agent/tools/research_web.ts
import { generateText } from "ai";
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description:
"Search the live web with a search-grounded model and return synthesized findings with source URLs. Use for recent thought leadership, analyst POVs, market news, and trends. One focused query per call.",
inputSchema: z.object({
query: z.string().min(1).describe("A focused research question or topic"),
recencyDays: z
.number()
.int()
.positive()
.max(90)
.default(14)
.describe("Only consider material from the last N days"),
}),
async execute({ query, recencyDays }) {
const { text, sources } = await generateText({
model: "perplexity/sonar-pro",
prompt: [
`Research the following for material published in the last ${recencyDays} days.`,
`Report concrete claims, named authors and firms, numbers, and publication dates.`,
`Prefer primary sources. Flag anything you could not verify.`,
``,
query,
].join("\n"),
});
const urls = (sources ?? [])
.filter((source) => source.sourceType === "url")
.map((source) => ({ title: source.title ?? null, url: source.url }));
return { findings: text, sources: urls };
},
});
The main agent runs on anthropic/claude-sonnet-5; research calls go to perplexity/sonar-pro, which is grounded in live search and returns source URLs. One gateway auth covers both. The instructions require at least three research calls per loop with different query angles, so coverage does not collapse into one query’s bias.
Markdown files as memory, typed tools over storage
Eve gives agents a sandbox with generic file tools, but the sandbox is ephemeral scratch space. Loop state and drafts need durable storage a human can review. So the agent gets three typed tools, list_content, read_content, and write_content, as its only sanctioned way to touch content/ paths. All three sit on one shared module with a path traversal guard, and the write tool only accepts .md files.
I built this local-first with node:fs against the repo’s content/ directory. Before deploying I swapped the store to Vercel Blob, because Vercel’s deployed filesystem is ephemeral and a cron-written draft would vanish. The swap touched exactly one file. The tools never knew where bytes lived, so nothing above the storage module changed:
// agent/lib/content-store.ts (abridged)
const useBlob = () => Boolean(process.env.BLOB_READ_WRITE_TOKEN);
export async function writeContentFile(relativePath: string, content: string) {
const pathname = normalizeContentPath(relativePath); // blocks ../ traversal
if (!pathname.endsWith(".md")) throw new Error("Only .md files are allowed");
if (!useBlob()) { /* local fs fallback */ }
await put(pathname, content, {
access: "private",
addRandomSuffix: false,
allowOverwrite: true,
contentType: "text/markdown",
});
return pathname;
}
Provisioning the store is one command, which also injects the token into the linked Vercel project and your .env.local:
vercel blob create-store eve-content --access private --yes
Because local dev and production share the token, they share the store. A draft the production cron writes on Monday morning is immediately readable from a local chat session. That one property makes the review workflow work.
One step that is easy to miss: the store starts empty. The repo’s content/ directory is the seed, so after creating the store, copy the initial files up with a short script that calls put() once per file. Mine is ten lines. Skip this and the first deployed run finds no positioning file and no calendar, and the loop has nothing to filter against.
The auth trap that breaks scheduled agents
This is the trap I flagged at the top, and it will bite anyone building an agent that runs on a schedule.
Eve’s registry ships a ready-made PostHog connection that uses per-user OAuth. It works in chat, because there is a signed-in user to run the OAuth flow. It fails in scheduled runs, because a cron-started session has no user principal. Instead of starting OAuth, the connection errors with reason: "principal_required" and your Monday run loses its analytics step.
The fix is app-scoped auth: a static token that belongs to the app, not to a user session. PostHog’s MCP server accepts a personal API key as a bearer token:
// agent/connections/posthog.ts
import { defineMcpClientConnection } from "eve/connections";
// App-scoped auth (personal API key) rather than per-user OAuth so the
// scheduled content loop can query PostHog without a signed-in user.
export default defineMcpClientConnection({
url: "https://mcp.posthog.com/mcp",
description:
"PostHog analytics for SandsDX: web analytics, insights, events, and SQL queries. Use to measure how published content performs.",
auth: {
getToken: async () => {
const token = process.env.POSTHOG_API_KEY;
if (!token) {
throw new Error(
"POSTHOG_API_KEY is not set. Create a PostHog personal API key and add it to .env.local.",
);
}
return { token };
},
},
});
Two details to get right. The key must be a personal API key, the kind with the phx_ prefix; a project token (phc_) will not authenticate against the MCP server. And read scopes for queries and insights are enough, so scope it down.
The model never sees the URL or the token. It discovers the PostHog tools through Eve’s built-in connection_search and calls them as posthog__<tool>. After deploying I tested it end to end: I asked for pageviews over the last 7 days, and the agent found the connection, ran the queries, and answered with the correct numbers from PostHog’s web overview.

There is a second reason app-scoped auth is mandatory here: a scheduled Eve session runs in task mode, meaning it fires, runs to completion, and cannot park to wait for a human or an OAuth sign-in. If a step might need interaction, the schedule design has to make it either app-authenticated or skippable. My instructions make the PostHog step skippable: on the first run, before the key was configured, the agent logged the skip in the insights file and kept going.
The schedule is a markdown file
The entire scheduling system, from my side, is this file:
---
cron: "0 12 * * 1"
---
Run the weekly content loop exactly as described in your instructions: review
PostHog performance and update insights, research the last 14 days of AI GTM
material for B2B SaaS, align findings to the conversations to own, update the
editorial calendar, write the blog post draft, and write the three LinkedIn
drafts. Everything lands in content/ as drafts.
On deploy, Eve compiles every schedule file into a real Vercel Cron Job. I wrote no vercel.json cron config. After vercel --prod:
$ vercel crons ls
> 1 cron job found for page-sandsdxcoms-projects/eve
Path Schedule
/eve/v1/cron/A5D6HkIVP8iU5RQjv3B_b-ECmVLOuVsYu0xe1N2eEnM 0 12 * * 1
Two things about that cron line. Vercel evaluates cron in UTC, so 0 12 * * 1 is Monday 8am Eastern in summer. And eve dev never fires schedules on a cadence; locally you trigger a run by hitting a dev-only dispatch route:
curl -X POST http://localhost:3000/eve/v1/dev/schedules/content-loop
# -> { "scheduleId": "content-loop", "sessionIds": ["wrun_..."] }
You can then stream the run live from GET /eve/v1/session/<id>/stream, which emits newline-delimited JSON: tool calls, message deltas, and per-step token usage with cost. Watching the loop execute step by step in that stream is the fastest way to debug the instructions. Cost has been a non-issue so far; a simple agent turn through the gateway runs about a tenth of a cent, and the stream shows usage.costUsd per step so there are no surprises.
Voice as a skill, and an agent that grades its own drafts
Brand voice lives in agent/skills/sandsdx-voice.md: banned vocabulary, banned rhetorical constructions, citation requirements, and a self-check instruction. The skill’s frontmatter description tells the model when to load it, and Eve loads the body on demand. Existing SKILL.md files from the Agent Skills convention port over as-is.
What the agent did with those rules surprised me. On the first run, it ran grep -niE "leverage|synergy|holistic|..." over its own drafts in the sandbox before finishing, and used the sandbox to count each LinkedIn draft against the 1300-character limit. A skill written as a checklist turned into behavior I could verify in the session stream.
The same rule set produced editorial judgment I did not script. The first run’s calendar has a backlog entry the agent refused to draft, with this reasoning written into the file (quoted with the specific vendor names removed):
Signal-based pipeline, with numbers we can actually stand behind. Held back this run: the reply-rate and lift benchmarks currently in circulation [names five vendors] come from SEO-style vendor content with no visible authorship, methodology, or publication date. Do not publish these numbers as sourced fact. Next loop: look for a named analyst […] or a named operator with a disclosed dataset before writing this one.
A “data over adjectives” rule in the voice skill, applied by the agent to its own research, held a topic out of the pipeline because the sourcing was weak. That is the quality gate doing its job unprompted.

What the first run produced
I dispatched it once through the dev route. The run skipped PostHog cleanly and logged why, ran its research queries, wrote a sourced blog draft setting Forrester’s 2025 Buyers’ Journey Survey data against SaaStr’s 20-agent SDR case study with every claim linked to a primary source, wrote three LinkedIn drafts in the required formats, and populated the calendar with a five-item ranked backlog.
It also hung.
Every deliverable was done, and then the run sat indefinitely on its final self-check. The agent had piped draft text into python3 -c "... open('/dev/stdin').read()", which blocks forever on an empty stdin, and a task-mode session will happily wait on a blocked command until the end of time. The fix was a new instructions rule: sandbox commands must be non-interactive and take file arguments, grep against files, never stdin pipes. If your agent runs shell commands on a schedule, add that rule before your first hung run instead of after, which is the order I did it in.
The gotchas, collected
- Node 24 or bust. The engines check fails on Node 22 with no workaround.
- Per-user OAuth connections fail scheduled runs with
principal_required. Scheduled agents need app-scoped tokens. - PostHog’s MCP server wants a personal API key (
phx_), not a project token (phc_). - Vercel cron is UTC.
0 12 * * 1is not noon in your timezone unless you live in UTC. - The deployed filesystem is ephemeral. Anything a cron run writes needs Blob or another real store.
- Env var changes after a deploy require a redeploy to take effect.
- Agent shell commands that read stdin can hang a task-mode run forever. Require file arguments.
- The scaffold’s
placeholderAuth()hard-blocks browser access to the deployed agent, which is correct until you wire real auth. Production is cron-plus-API only; humans chat through local dev against the shared Blob store.
Where this fits
Most content-agent demos are a text box that generates a post. Signal sync was not a system yet when I wrote about collecting signals by hand, and a generate-a-post demo would not be one either. A system needs strategy input it cannot override, quality gates it applies to itself, memory that outlives a session, and a feedback path from real performance data.
The constraints in this build are exactly those parts: a positioning file the agent reads but cannot edit, a voice skill it greps its own drafts against, an insights file that turns PostHog data into next week’s headline choices, and a schedule that fires whether or not I show up. The self-improving content agent is the loop around the generation, and the loop is where the engineering went.
The draft it writes every Monday still crosses a human desk before it ships. On current evidence, the agent would be the first to insist on that.