What it does
marketing-team-eve-template is a Vercel Labs template for running a team of marketing agents on eve. You talk to a lead in Slack or a terminal — "draft the launch announcement," "review this page's SEO" — and it briefs one of five specialists who does the actual work: writing to Notion, queuing posts in Typefully, sending campaigns through Resend.
Why I starred it
Most multi-agent templates I've seen either let every agent call every tool, or bolt on a permissions system that's more code than the agents themselves. This one does neither. The lead never touches a deliverable — it routes once, with a full brief, to exactly one specialist. And the specialists don't delegate further; they research and edit their own work rather than spawning sub-subagents. That's a shallow, legible hierarchy, and the repo is disciplined about keeping it that way.
The other thing that got me: this is 448 stars and about five real dependencies (eve, ai, zod, @vercel/blob, @vercel/connect). No framework sprawl. The complexity is all in how the agents are briefed and gated, not in the dependency graph.
How it works
The lead's whole runtime config is agent/agent.ts:
export default defineAgent({
compaction: { thresholdPercent: 0.9 },
model: "anthropic/claude-opus-5",
});
Everything else — channels, connections, tools, subagents — is discovered from the filesystem under agent/. There's no central registry: a subagent's name is its directory name under agent/subagents/, a connection's name is its filename. docs/ARCHITECTURE.md states the rule directly — "a tool's name is its filename... eve walks agent/ at build time and produces a manifest from what it finds." Add a specialist by adding a directory; remove one by deleting it.
The five specialists — product-marketer, content-marketer, social-media-coordinator, seo, email — each get routed to by a description string in their own agent.ts, which is literally all the lead sees when deciding who handles a request. social-media-coordinator/agent.ts reads like a job posting: platforms it owns, what it hands back, and "the caller passes the brief or source material... in the message" — a reminder that subagents start with zero shared history, so the brief has to be self-contained.
The one piece of state all specialists share is a brand-context document, stored in Vercel Blob. agent/lib/brand-context/tools.ts defines get_brand_context and save_brand_context as factories rather than shared constants, specifically so each agent's one-line tools/get_brand_context.ts file owns its own tool instance instead of re-exporting one. The save tool has no approval gate — deliberately:
// This overwrites the whole document for everyone and cannot be undone,
// so load the current context first, merge the new information into it,
// and save the full result. Show the user what you're about to save and
// get their agreement before calling this.
The protection is entirely in the tool description and the calling agent's instructions, not in code. product-marketer is the only specialist that writes it; the other three only read it.
The part worth reading closest is the approval logic on the external connections. agent/subagents/email/connections/resend.ts narrows Resend's ~85 MCP tools down to a 46-item ALLOWED_TOOLS allow-list — no create-api-key, no remove-domain, no webhook CRUD — with a comment explaining that an allow-list means "a tool the server adds later is invisible here until someone adds it on purpose." On top of the allow-list, SEND_TOOLS and DESTRUCTIVE_TOOLS gate on substring match against the tool name:
approval: ({ toolName }) =>
[...SEND_TOOLS, ...DESTRUCTIVE_TOOLS].some((tool) =>
toolName.includes(tool)
)
? "user-approval"
: "not-applicable",
send-broadcast mails a whole segment the instant it runs and can't be recalled, so it's always gated — no conditional logic needed. Typefully's typefully_create_draft and typefully_edit_draft, in social-media-coordinator/connections/typefully.ts, get a smarter version: they only pause for approval when the call actually schedules something, which the code checks by reaching into requestBody.publish_at:
const readPublishAt = (input: unknown): unknown => {
if (typeof input !== "object" || input === null) return;
const body = (input as { requestBody?: unknown }).requestBody;
if (typeof body !== "object" || body === null) return;
return (body as { publish_at?: unknown }).publish_at;
};
Saving a plain draft stays friction-free; setting publish_at flips the same tool call into a gated one. That's a more precise approval model than most MCP wrappers bother with — most either gate a tool entirely or not at all.
agent/channels/slack.ts has its own small pile of edge cases worth noting. slackSessionAuth rebuilds eve's session auth because the default derivation "attaches no auth at all when it cannot parse the message author," which happens on Slack's Connect webhook path. There's also isSoleThreadParticipant, which keeps un-mentioned auto-replies scoped to the original requester — once a second human joins the thread, the bot stops replying without an explicit @mention, checked by pulling the thread's first 50 participants and failing closed if the list is empty.
Using it
The one-click Vercel deploy wires up Notion, Resend, and Slack connectors plus a Vercel Blob store, and prompts for a Typefully API key. Locally:
vercel link
vercel env pull
pnpm dev # then run /model once in the TUI to link a provider
npx eve info prints the full discovered surface — every tool, skill, connection, and subagent eve found by walking the filesystem, which doubles as a sanity check that a new specialist directory actually got picked up. pnpm validate chains lint, typecheck, and that same discovery diagnostic into one command.
Rough edges
There are no automated tests anywhere in the repo — no *.test.ts, no evals/ beyond an empty scaffold referenced in package.json's imports map. For a template whose entire value proposition is "the approval gates are correct," that's a gap I'd want closed before trusting it with a real Resend account. The gating logic (toolName.includes(tool)) is also a substring match, which the resend.ts comments acknowledge directly: remove-contact matching also catches remove-contact-from-segment, which is harmless here but is the kind of pattern that bites you the day you add a tool name that happens to be a prefix of something you meant to leave open.
Git history is thin — six commits total since the template shipped, mostly docs reshuffling and a dependency bump. That's normal for a reference template rather than a living product, but it means the approval matrix hasn't been stress-tested against real usage patterns in the open.
Bottom line
If you're building a multi-agent system and wondering how deep delegation should go or how to gate destructive tool calls without writing a permissions framework, read agent/subagents/email/connections/resend.ts and agent/channels/slack.ts end to end. This is a template to learn the pattern from, not (yet) a production system to trust unmodified with a live email list.
