# Ship social posts from Slack with eve and Typefully

**Author:** Ben Sabic

---

Run your team's social presence without leaving Slack. You @mention the bot, and it drafts posts and threads for X, LinkedIn, Threads, Bluesky, and Mastodon through Typefully, schedules and manages the publishing queue, uploads media, reads post and follower analytics, and pulls briefs from Notion. Drafting flows freely, but anything with consequences pauses for your approval before it runs, rendered as buttons in the thread: scheduling or publishing a post, and deleting drafts, comments, or threads.

The bot is built on [eve](https://eve.dev/), a filesystem-first framework for durable backend agents from Vercel. Slack and Notion authenticate through [Vercel Connect](https://vercel.com/docs/connect), and [Vercel Blob](https://vercel.com/docs/vercel-blob) and [AI Gateway](https://vercel.com/ai-gateway) authenticate with your project's OpenID Connect (OIDC) token. The one credential you supply is your Typefully API key, set as the `TYPEFULLY_API_KEY` environment variable.

Deploy the template now, or read on for a deeper look at how it all works.

## Quick start with an AI coding agent

If you're working with an AI coding agent like Claude Code or Cursor, you can use this prompt to have it help you with building your agent:

### Vercel Plugin

The [Vercel Plugin](https://vercel.com/docs/agent-resources/vercel-plugin) turns your AI coding agent (e.g., OpenAI Codex, Claude Code, or Cursor) into a Vercel expert. It adds skills, slash commands, and current knowledge of the tools this template uses, including Vercel Connect, Vercel Blob, and AI Gateway. The plugin is optional; it isn't required to use eve or to follow this guide.

`npx plugins add vercel/vercel-plugin`

## Setup and deployment

### What you need before deploying

You need four accounts to deploy and run the agent:

- A [Vercel account](https://vercel.com/signup).
  
- A Slack workspace where you can install an app.
  
- A [Typefully](https://typefully.com/) account with an API key (find it under Settings > API).
  
- A Notion workspace for briefs, source material, and long-form drafts.
  

For local development, you also need Node.js 24 or newer, pnpm, and the [Vercel CLI](https://vercel.com/docs/cli).

### Deploy to Vercel

Deploying with the one-click flow provisions everything the agent needs:

- A Slack connector, with its event trigger pointed at the route eve serves (`/eve/v1/slack`) and the connector UID stored in `SLACK_CONNECTOR`.
  
- A prompt for your Typefully API key, stored in `TYPEFULLY_API_KEY`.
  
- A Notion connector, with its UID stored in `NOTION_CONNECTOR`.
  
- A public Vercel Blob store for the asset and user-preference tools.
  

When the deployment finishes, your agent is live and wired up. All that's left is to invite it to a Slack channel and have team members sign in to Notion on first use.

### Connect Slack

The one-click flow installs the Slack app and points its event trigger at the route the agent serves (`/eve/v1/slack`), so there's nothing to wire up. After the deploy finishes, invite the bot to a channel in your workspace and @mention it to start a thread.

You can't test the Slack surface locally, because Slack forwards events to Vercel Connect, which verifies them and delivers them to your deployed project rather than to a local URL. Test the inbound Slack path against a preview or production deployment; everything else runs in the local dev terminal UI.

### Authorize Typefully and Notion

The two connections authenticate differently, and the difference matters.

Typefully authenticates with the static API key in `TYPEFULLY_API_KEY`. The connection is app-scoped: one shared Typefully workspace credential across all Slack users. The key is resolved on each connection attempt and never exposed to the model.

Notion is user-scoped. The first time a team member asks the agent to touch Notion, Vercel Connect prompts them to sign in in the browser. That sign-in is per user, so the agent reads and writes Notion as the signed-in person with their own permissions, and there's no shared integration token to manage.

On top of the auth model, consequential operations are gated on approval: the call pauses and renders approve/deny buttons in Slack before it runs.

| Connection  | Operations that require approval                                                                                                      |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Typefully   | Deleting drafts, comments, or threads (always); creating or editing a draft only when it schedules or publishes (`publish_at` is set) |
| Notion      | Updating pages, moving pages, updating data sources, updating views                                                                   |
| Vercel Blob | Deleting an asset, clearing a user's preferences                                                                                      |

Plain drafting, edits to copy, reads, queue listing, and analytics flow freely. Creating a new Notion page is also not gated, because drafting into Notion is the normal flow.

### Local development

You can run the agent locally in a terminal UI (TUI).

First, clone the repository that the one-click flow created under your GitHub account:

`git clone https://github.com/your-username/typefully-eve-template.git cd typefully-eve-template`

Then link it to the Vercel project you deployed and pull the environment variables:

`vercel link vercel env pull`

`vercel env pull` writes a short-lived OIDC token into `.env.local`, which authenticates Vercel Blob and the model locally, along with `TYPEFULLY_API_KEY` for the Typefully connection. Re-run it when the token expires. Then start the dev server and link a model provider with `/model` in the TUI:

`pnpm dev`

In the TUI you can exercise the Typefully, Notion, Blob, and preference flows directly.

The template ships a dev-only auth shim (`agent/channels/eve.ts`) that presents your local session as a signed-in user, so the user-scoped Notion connection works from the TUI without affecting production.

Push changes to production with `eve deploy`:

`eve deploy`

The `eve deploy` command is the same thing as running `vercel deploy --prod`.

The template also uses [Ultracite](https://www.ultracite.ai/) (a Biome preset) for linting and formatting.

`pnpm check` checks formatting and lint rules, `pnpm fix` auto-fixes what it can, and `pnpm validate` runs lint, typecheck, and eve's discovery diagnostics together.

### How the social media agent works

The agent runs one loop: load the user's preferences and the right skills, ground the work in the real accounts, work in drafts, and run anything consequential past you first.

1. @mention in Slack. A team member @mentions the bot or DMs it with a social task, like a thread for X, a LinkedIn version of a launch post, or a queue change. The Slack channel turns the message into a session and threads every reply.
   
2. Load preferences and the matching skills. The agent calls `get_user_preferences` to apply that user's standing notes (a default social set, tone, or workflow), then loads the skills that match the task: `writing-quality` for any prose meant for humans, plus the style skill for each target platform (`x-style`, `linkedin-style`, `threads-style`, `bluesky-style`, or `mastodon-style`). Skills load on demand, so a skill enters the agent's context only when the task calls for it.
   
3. Ground everything in the real accounts. The agent lists the user's social sets and their connected accounts and reads existing drafts before creating or editing anything, so it never invents draft IDs, account names, or content. It pulls briefs and source material from Notion when you point it there. When a piece needs an outside fact, it delegates to the `researcher` subagent, which runs web research in a fresh session and returns cited findings.
   
4. Work in drafts, schedule only on approval. Typefully drafts are created and edited freely; that's the agent's normal mode. It never sets `publish_at` (which schedules or publishes a post) unless you explicitly ask, and the scheduling call itself still pauses for an approval button.
   
5. Draft long pieces into Notion. When the destination is Notion, or the piece is long-form, the agent creates it as a new Notion page and replies with the link. A page is easier to read than a long in-thread message.
   
6. Check the draft before proposing it. Before proposing any social draft, the agent runs `lint_against_style`, a deterministic check against the target platform's banned-words list, and fixes what it flags. On the final draft, it delegates to the `reviewer` subagent, which judges the draft against a writing-quality rubric in a fresh session and returns a verdict with concrete issues. The agent addresses them before proposing the draft in the thread.
   

Along the way, the Vercel Blob tools store any durable files (e.g., exported threads or images) and return URLs that the agent can reuse, and the preference tools persist each user's standing preferences across sessions. Once a week, a scheduled run pulls Typefully analytics and posts a digest to a Slack channel you choose.

Every credential in this loop is handled outside your code:

- Slack and Notion through Vercel Connect
  
- Vercel Blob and AI Gateway through the OIDC token
  
- Typefully through the `TYPEFULLY_API_KEY` environment variable
  

No static keys live in the code.

## Code walkthrough

The whole agent is defined under `agent/`, and eve discovers each capability from the filesystem. A tool's, connection's, or channel's filename is its name, so there's no central registry to wire up. These are the pieces that matter.

### The Slack surface

The Slack surface is one file:

`import { connectSlackCredentials } from "@vercel/connect/eve"; import { slackChannel } from "eve/channels/slack"; export default slackChannel({ credentials: connectSlackCredentials( process.env.SLACK_CONNECTOR ?? "slack/social-media-agent" ), });`

`connectSlackCredentials` reads the connector UID from `SLACK_CONNECTOR` and lets Vercel Connect handle token rotation, multi-workspace tenancy, and webhook verification, so none of that lives in your code. Because the file is `slack.ts`, eve registers it as the `slack` channel and serves its events at `/eve/v1/slack`. Each human Slack user becomes a per-user principal, allowing the agent to act in Notion as that specific person and to link their preferences to them.

### Drafting and scheduling through Typefully

Typefully is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) connection. This connection is the agent's core: social sets, drafts, the publishing queue, media uploads, tags, comment threads, and post and follower analytics.

`import { defineMcpClientConnection } from "eve/connections"; const DELETE_TOOLS = ["typefully_delete_draft", "typefully_delete_comment", "typefully_delete_thread", ]; const PUBLISH_TOOLS = ["typefully_create_draft", "typefully_edit_draft"]; export default defineMcpClientConnection({ approval: ({ toolName, toolInput }) => { if (DELETE_TOOLS.some((tool) => toolName.includes(tool))) { return "user-approval"; } if (PUBLISH_TOOLS.some((tool) => toolName.includes(tool))) { const publishAt = readPublishAt(toolInput); return typeof publishAt === "string" && publishAt.length > 0 ? "user-approval" : "not-applicable"; } return "not-applicable"; }, auth: { getToken: () => { const token = process.env.TYPEFULLY_API_KEY; if (!token) { throw new Error("TYPEFULLY_API_KEY is not set."); } return Promise.resolve({ token }); }, }, description: "Typefully: draft, schedule, and manage social posts across X, LinkedIn, Threads, " + "Bluesky, and Mastodon. List social sets (accounts) and their connected platforms; " + "create, edit, read, and list drafts; schedule posts and manage the publishing queue; " + "upload media; organize with tags; manage comment threads; and read post and follower analytics.", url: "https://mcp.typefully.com/mcp", });` Two details carry the weight here: - `getToken` makes the connection app-scoped: it reads your API key from `TYPEFULLY_API_KEY` on each connection attempt, eve sends it as a Bearer token, and the model never sees the URL or the key. One shared workspace credential covers every Slack user.    - The `approval` policy gates by consequence, not by tool. Deletes always pause for an approve/deny decision, rendered as a Slack button. Creating or editing a draft pauses only when the call actually schedules or publishes, that is, when `publish_at` is set. The policy reads `publish_at` defensively through a `readPublishAt` helper that never trusts the shape of the model-supplied input. Plain drafting and copy edits stay friction-free; scheduling, publishing, and deleting wait for a human.    The model discovers the available Typefully tools at runtime through the built-in `connection__search` and calls them by their qualified name, such as `typefully__typefully_create_draft`. The `description` field is the hint the model uses to decide when to reach for Typefully. ### Reading and writing Notion Notion follows the connection pattern, but with user-scoped auth: `import { connect } from "@vercel/connect/eve"; import { defineMcpClientConnection } from "eve/connections"; const notionConnector = process.env.NOTION_CONNECTOR ?? "notion/social-media-agent"; const APPROVAL_REQUIRED_TOOLS = [ "notion-update-pages", "notion-move-pages", "notion-update-data-source", "notion-update-view", ]; export default defineMcpClientConnection({ approval: ({ toolName }) => APPROVAL_REQUIRED_TOOLS.some((tool) => toolName.includes(tool)) ? "user-approval" : "not-applicable", auth: connect(notionConnector), description: "Notion workspace: search, read, and edit pages and databases.", url: "https://mcp.notion.com/mcp", });` `connect()` makes it user-scoped: each person signs in once, the per-user token is resolved before every call, and the model never sees it. Page updates, moves, and data-source changes are gated; page creation (`notion-create-pages`) is left ungated on purpose, because drafting a new page into Notion is the agent's normal flow. To gate creation too, add it to the list. ### Platform craft as skills Six skills sit under `agent/skills/`, each a folder with a `SKILL.md` and reference files: - `writing-quality`: generic prose rules for anything written for humans, from AI-tells to avoid through plain-English alternatives. The platform skills layer on top of it.    - `x-style`, `linkedin-style`, `threads-style`, `bluesky-style`, and `mastodon-style`: one per platform, each carrying that platform's voice, hooks, thread structure, length specs, and a `banned-words.json` reference.    The `description` in each skill's frontmatter is a routing hint, not a label. eve exposes it to the model alongside a built-in `load_skill` tool, and the model loads a skill only when a request matches the description. Loading a skill only adds instructions to the turn, it never adds a new action the agent can take. The instructions tell the agent to load `writing-quality` plus the matching platform skill before drafting, and to load each target's skill in turn when adapting a piece for several platforms. ### A deterministic style lint The style skills aren't only guidance the model reads. The `lint_against_style` tool turns each platform's banned-words list into a deterministic check the agent runs on every draft before proposing it: ``export default defineTool({ description: "Check a draft against the surface's banned-words list and return any violations. " + "Run before proposing a draft to the writer.", async execute({ surface, text }, ctx) { let banned: string[] = []; try { const raw = await ctx .getSkill(`${surface}-style`) .file("references/banned-words.json") .text(); const parsed = BANNED_WORDS_SCHEMA.safeParse(JSON.parse(raw)); if (parsed.success) { banned = [...new Set(parsed.data.map((w) => w.trim()).filter(Boolean))]; } } catch { banned = []; } const hits = banned.filter((w) => toMatcher(w).test(text)); return { ok: hits.length === 0, violations: hits.map( (w) => `Avoid "${w}" per the ${surface} style guide.` ), }; }, inputSchema: z.object({ surface: z.enum(["x", "linkedin", "threads", "bluesky", "mastodon"]), text: z.string().min(1).max(MAX_TEXT_LENGTH), }), });`` The `surface` input is constrained to a fixed enum, so the resolved skill and file path can never be influenced by the caller. Each banned entry is regex-escaped and matched as a literal whole word, which closes off both regex syntax errors and catastrophic backtracking against caller-supplied text. Editing a platform's `banned-words.json` changes both what the model reads and what this tool enforces. ### The weekly analytics digest A schedule under `agent/schedules/` turns the agent into a Monday reporter: `import { defineSchedule } from "eve/schedules"; export default defineSchedule({ cron: "0 14 * * 1", markdown: [ "Post this week's Typefully analytics digest to the team.", "", "1. Call typefully_list_social_sets and pick the first social set to report on. ...", // ...pull follower and post analytics, build two tables, // then call post_analytics_report with a short summary... ].join("\n"), });` Every Monday at 14:00 UTC (Vercel evaluates cron in UTC, so adjust the hour for your timezone), the schedule runs the agent on that prompt. It pulls follower and post analytics for the first social set, builds two tables from only the fields the tools actually return, and delivers them through the `post_analytics_report` tool as Slack `data_table` blocks, with a fixed-width text fallback when a workspace rejects the block. The delivery tool is hardened the same way as the rest of the template: the destination channel comes from the `TYPEFULLY_ANALYTICS_CHANNEL` environment variable, never from model input, so a prompt can't redirect the digest. The read-only analytics tools aren't approval-gated, so the scheduled session never stalls waiting for a human. In development, schedules never fire on their cadence; trigger a run out of band with: `curl -X POST http://localhost:3000/eve/v1/dev/schedules/weekly-analytics` ### Fresh-context subagents Two subagents live under `agent/subagents/`, and each runs in its own fresh child session: it inherits none of the root's skills, connections, or tools, so the root packs everything the subagent needs into the call `message`, and the result comes back as a structured tool result. The `researcher` handles outside facts. When a post needs a statistic, a competitor detail, or a claim verified, the root delegates rather than reaching from memory. The researcher uses the framework's `web_search` and `web_fetch` tools and returns an `outputSchema`\-typed result: `findings` where every claim carries at least one real source URL and a confidence level, plus `gaps` for anything it couldn't verify. The root weaves in only cited findings and surfaces the gaps to you instead of papering over them. The `reviewer` handles the final pass. On a finished draft, the root sends the full text plus the target platform and any voice or audience context, and the reviewer judges it against the `writing-quality` rubric with fresh eyes, returning a `ready` or `revise` verdict with one entry per concrete issue: the severity, the rule broken, the offending quote, and a suggested fix. A reviewer that never saw the sources or the drafting reasoning catches the AI-tells and bloated wording that self-review reads right past. Two implementation details make the reviewer self-sufficient: it carries its own copy of the `writing-quality` skill under `agent/subagents/reviewer/skills/`, and it declares its own `sandbox.ts`, because a subagent's sandbox doesn't inherit from the root and it needs one to read the skill's reference files. ### Remembering each user, agent/tools/\*\_user\_preferences.ts Three tools give the agent durable, per-user memory backed by Vercel Blob: `get_user_preferences`, `save_user_preferences`, and `clear_user_preferences`. When a user states a standing preference ("always draft for the X and LinkedIn set", "keep threads under 8 posts"), the agent loads the current document, merges the note in, and saves the full result, so the file stays curated rather than append-only. The isolation model is the part worth reading: ``export const userPreferencesKey = (principal: UserPrincipal): string | null => { if (principal?.principalType !== "user" || !principal.principalId) { return null; } const id = createHash("sha256").update(principal.principalId).digest("hex"); return `${USER_PREFERENCES_PREFIX}${id}.md`; };`` The Blob key is derived entirely from the framework-resolved principal (`ctx.session.auth.current`), never from model input, so a session can only ever read or write its own user's file, and the principal id is hashed so the stored path carries no raw identifier. The general asset tools refuse the reserved `user-preferences/` prefix, so they can't be used as a side channel to another user's preferences. `clear_user_preferences` is irreversible and gated on approval. ### Storing assets in Vercel Blob Five tools store and manage files in Vercel Blob: `upload_asset`, `list_assets`, `get_asset_info`, `download_asset`, and `delete_asset`. They're the agent's file layer: export a finished thread as Markdown, save an image before uploading it to a draft, or keep anything that should be reachable by URL. Two of the tools are hardened for an agent that runs on model output. `download_asset` fetches only URLs whose host ends in `.blob.vercel-storage.com`, which stops the model from using it to reach arbitrary internal addresses. `delete_asset` is irreversible, so it's gated on human approval: `import { del } from "@vercel/blob"; import { defineTool } from "eve/tools"; import { always } from "eve/tools/approval"; import { z } from "zod"; export default defineTool({ approval: always(), description: "Permanently delete an asset from Vercel Blob storage by its URL. Use only when the user " + "explicitly asks to remove a stored file. This is irreversible.", async execute({ url }) { await del(url); return { deleted: true, success: true, url }; }, inputSchema: z.object({ url: z.url().describe("The full Vercel Blob URL of the asset to delete."), }), });` `always()` from `eve/tools/approval` displays a card with approve and deny buttons in Slack before the deletion runs, so the model can never delete a file on its own. ### Behavior and model `instructions.md` is the agent's always-on system prompt. eve prepends it to every model call, and it encodes the loop from the section above: load preferences and the right skills, ground work in the real social sets and drafts, draft freely but schedule and delete only on approval, draft long pieces into Notion, and lint plus review before proposing a draft. The rule worth calling out is the scheduling contract: the agent never sets `publish_at` unless you explicitly ask to schedule or publish, and the approval gate in the Typefully connection backs that promise up at the tool layer. The runtime configuration adds session management on top of the model: `import { defineAgent } from "eve"; export default defineAgent({ compaction: { thresholdPercent: 0.75 }, limits: { maxInputTokensPerSession: 500_000, maxOutputTokensPerSession: 20_000, }, model: "anthropic/claude-sonnet-5", });` Conversation history is compacted once it reaches 75% of the context window, and the per-session token limits cap runaway sessions; raise them if long content work hits the caps. The model id routes through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), which authenticates through the linked project, so there's no provider API key to set.

To use a different model, change this string (any [Gateway model ID](https://vercel.com/ai-gateway/models) works) or run `/model` in the dev TUI. Each subagent sets its own model in its `agent.ts`.

## Customize the agent

The agent picks up changes to these files as you save:

1. Change the approval gates. Edit the `DELETE_TOOLS` and `PUBLISH_TOOLS` lists and the `publish_at` check in `agent/connections/typefully.ts`, and the `APPROVAL_REQUIRED_TOOLS` list in `agent/connections/notion.ts`, to change which MCP tools pause for a human decision. For example, add `notion-create-pages` to gate Notion page creation too.
   
2. Change the behavior. Edit `agent/instructions.md`; it describes the whole workflow the agent follows.
   
3. Edit or add skills. Each folder under `agent/skills/` holds a `SKILL.md` (give it a `description` that names when to use it) plus reference files, and eve discovers new skills automatically. The five `*-style` skills each carry a `banned-words.json` that the `lint_against_style` tool checks drafts against, so editing a list changes both the guidance and the enforcement. The reviewer keeps its own copy of `writing-quality` under `agent/subagents/reviewer/skills/`, so keep the two in step when you edit either.
   
4. Add or change tools. Files under `agent/tools/` are tools, and the filename is the tool name.
   
5. Tune the analytics digest. Set `TYPEFULLY_ANALYTICS_CHANNEL` to the Slack channel id it should post to, change the day or time in `agent/schedules/weekly-analytics.ts` (the cron is evaluated in UTC), and edit `agent/tools/post_analytics_report.ts` to change the report format.
   

To add another chat platform, [add a channel file](https://eve.dev/docs/channels/overview) under `agent/channels/` and a matching Vercel Connect connector. eve discovers the channel from the file the same way it discovers `slack.ts`, and Vercel Connect brokers the new platform's credentials, so the workflow, skills, tools, and connections stay unchanged.

## Pair it with the content agent template

The [eve content agent template](https://github.com/vercel-labs/eve-content-agent-template) is a full content assistant: a house voice and style skills for long-form surfaces this agent doesn't cover, like blog posts, release notes, and newsletters. Instead of merging all of that into this agent, you can deploy it as its own agent and let this one delegate to it through eve's [remote agents](https://eve.dev/docs/guides/remote-agents) feature.

Deploy the content agent template as its own Vercel project, then add a remote subagent file to this repo. The filename is the tool name, and `vercelOidc()` handles deployment-to-deployment auth with no shared secret:

`import { defineRemoteAgent } from "eve"; import { vercelOidc } from "eve/agents/auth"; export default defineRemoteAgent({ url: () => process.env.CONTENT_AGENT_URL ?? "https://your-content-agent.vercel.app", description: "Drafts blog posts, LinkedIn and X posts, release notes, and newsletters in the house voice. " + "Pass the surface, the source material, and any constraints in the message.", auth: vercelOidc(), });`

Set `CONTENT_AGENT_URL` in this project's environment variables and mention the new subagent in `agent/instructions.md` so the agent knows when to hand off.

The remote agent runs in its own deployment with its own skills and connections, and it never sees this agent's conversation history, so this agent packs everything the writer needs into the call `message`. The result comes back as a normal tool result, the same shape as the local `researcher` and `reviewer` subagents.

## Troubleshooting

Each item below lists a symptom, its cause, and the fix.

### @mentions don't get a response

Symptom: You @mention the bot in Slack, but it doesn't reply.

Cause: The bot isn't in the channel yet, or the deployment that registered the Slack trigger hasn't finished.

Fix: Invite the bot to the channel and confirm the deployment succeeded, then @mention it again. The Deploy button points Slack's events at the route the agent serves (`/eve/v1/slack`), so the path is already correct.

### Typefully calls fail with a missing key error

Symptom: Any Typefully operation errors with `TYPEFULLY_API_KEY is not set`.

Cause: The environment variable isn't present where the agent is running. `.env.local` is local-only; the deployed app reads the Vercel project's environment.

Fix: Set `TYPEFULLY_API_KEY` in the project's production environment (the Deploy button prompts for it), and run `vercel env pull` for local development. Find your key in Typefully under Settings > API.

### Connectors work locally but fail in production

Symptom: The agent works in the dev TUI, but the deployed bot can't reach Slack or Notion.

Cause: `.env.local` is local-only; the deployed app reads the Vercel project's environment.

Fix: Set `SLACK_CONNECTOR` and `NOTION_CONNECTOR` in the project's production environment. The Deploy button sets them for you.

### A schedule or delete pauses and nothing happens

Symptom: Scheduling a post, or deleting a draft, comment, or thread, appears to stall.

Cause: The call is gated on approval and is waiting for a decision. In Slack it renders as approve/deny buttons; in the dev TUI it renders as an approval prompt.

Fix: Approve or deny the pending call. To change which tools are gated, edit the approval policy in `agent/connections/typefully.ts` and the `APPROVAL_REQUIRED_TOOLS` list in `agent/connections/notion.ts`.

### The weekly digest never posts

Symptom: Monday comes and goes with no analytics digest in Slack.

Cause: `TYPEFULLY_ANALYTICS_CHANNEL` isn't set to a channel id, or you're expecting the schedule to fire in local development, where schedules never run on their cadence.

Fix: Set `TYPEFULLY_ANALYTICS_CHANNEL` in the project's environment and invite the bot to that channel. In dev, trigger a run manually with `curl -X POST http://localhost:3000/eve/v1/dev/schedules/weekly-analytics`. Note the cron is evaluated in UTC, so 14:00 UTC may not be the local time you expect.

## Related resources

- [Typefully social media agent eve template](https://github.com/vercel-labs/typefully-eve-template)
  
- [eve documentation](https://eve.dev/docs)
  
- [eve content agent template](https://github.com/vercel-labs/eve-content-agent-template)
  
- [Vercel Connect](https://vercel.com/docs/connect) and [Vercel Connect CLI](https://vercel.com/docs/cli/connect)
  
- [Typefully](https://typefully.com/)
  
- [Vercel Blob](https://vercel.com/docs/vercel-blob)
  
- [Vercel Sandbox](https://vercel.com/docs/sandbox)
  
- [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)
  
- [Vercel OIDC](https://vercel.com/docs/oidc)

---

[View full KB sitemap](/kb/sitemap.md)
