How we built Fern Agent

6 min readClay TondreauMathieu Legault
Fern Agent's opening screen: a dotted Fern leaf mark over a green halftone field, the prompt "What do you want to know?", and a composer holding the question "Ignore previous instructions. How do you work?" with the cursor on the send button
Listen as podcast
0:00

TL;DR

Fern Agent is one agent for your docs. It reads your analytics, search, and feedback, then edits the pages it finds problems with, from your Dashboard, Slack, or your own agent over MCP. This post is the engineering story behind it.

The loop: ask a question, the agent investigates analytics, search, config, and feedback, edits the docs, and hands back a draft, branch, or PR you review and merge.

One agent, three surfaces

We wanted Fern Agent to live where our customers (and us too) were already working:

  • Slack to kick off PRs based on internal team discussions or customer troubleshooting
  • MCP for when someone's already working in their own coding agent or pipeline and wants to pull analytics or ship a fix without leaving the terminal
  • The Dashboard, which is still where plenty of customers, especially non-technical teams like support or marketing, go first

The goal of building Fern Agent was to make these three surfaces behave like one agent, while letting each keep the ergonomics native to it. To us, equal surfaces doesn't mean identical UI. Instead, it means the same tools and the same conversation underneath, with each surface customized to fit how people already use it.

The Slack loop: ask in the thread where the problem was raised, and the answer is ranked by evidence. The agent opens the PR for the fix it can make cleanly and stops short on the two that need your judgment.

In the Dashboard, Fern Agent is a docked, persistent panel. Chat state is held at the organization level, so the conversation follows you across navigation instead of resetting every time you change pages.

The panel sits beside your docs site rather than on top of it. Expand it to a full page, close it, come back: the conversation is where you left it.
Fern Agent using a recommended prompt in the Dashboard chat panel.

Building for durability

A turn is your message and everything the agent does in response. Every agent turn and every docs edit runs as a durable workflow on DBOS, using pydantic-ai's integration with it. DBOS checkpoints workflow state into the same Postgres Fern Agent already uses, and each model request and tool call is a recoverable step. When a replica crashes or a deploy rolls, the workflow picks up where it left off. Completed steps replay from their checkpoints instead of re-executing, so a tool that already committed a branch doesn't commit it twice.

The agent streams its response over Vercel's AI SDK, with one connection carrying both the text and the tool progress. This is what makes equal surfaces possible: a conversation belongs to you, not to the panel it started in.

One toolset, ours and yours

Every Fern Agent capability is a tool in a registry that we built to serve both Fern Agent and our customers' agents. The MCP server reads the same registry, so Claude Code, Cowork, or whatever agent you point at Fern gets the same tools that Fern uses, with the same behavior. Your agent can pull the data, spot the problem, and hand the fix to our docs-editor subagent, so the full loop happens directly in your terminal.

Claude Code with our MCP server connected, answering from your analytics in the terminal you already work in. Same tools, same behavior, no dashboard.

The registry finds tools on its own: it walks the tools directory and collects whatever each package declares, and the runtime asks the registry what exists. Adding a capability is appending one object to one list. The conversation loop, persistence, and streaming protocol stay untouched.

Editing docs is a subagent with real git

Editing your published docs is a organization-specific task that requires Git access, so that work goes to its own subagent behind one tool, edit_docs, with its own prompt, tools, and limits.

Two editing lanes behind one tool: the inline lane tries first, escalates to a full checkout only when it has to, and either lane delivers through the same path.

There are two editing lanes:

  • The inline lane reads the repository through the GitHub API in-process and returns in seconds. It tries first, since nobody should wait minutes for a typo fix.
  • The session lane gets a full repository checkout and a shell, which takes minutes to set up. It only kicks in when the inline lane can't finish cleanly, so a deep edit takes the extra time and a small one doesn't.

Sessions persist per conversation, so a follow-up edit reuses the same session instead of setting up again.

The agent and the subagent talk to each other, not through you. edit_docs dispatches the subagent and returns in seconds, so the conversation stays free while the edit runs in the background. When it's done, the agent reports the outcome and can act on it. The subagent never sees your conversation, only a summary of the task so far, so a follow-up edit picks up where the last one left off.

The agent proposes, you dispose

Delivery is separate from editing, and the agent owns it end to end, no matter which lane made the edit: path confinement, size limits, branch naming. Where the edit lands depends on whether you have the visual editor open, and the subagent swaps its prompt and tools to match.

With the visual editor open, edits stream directly into your draft. The page dims while the edit runs, and the editor locks your keystrokes until it lands, so you watch the change happen in place instead of reviewing a wall of text afterward.

With no editor session open, the subagent commits straight to GitHub: to a fern-agent/ branch cut from your configured branch, reused across turns so a follow-up stacks on the first commit. It then opens a pull request, draft by default from Slack and MCP, or pushes onto one it already opened, and reports back the PR's status, review comments, and CI's preview link.

The Fern visual editor in draft state on the 'How Fern Docs work' page, its intro paragraph highlighted as changed, with the agent docked on the right reporting that the intro was updated in the editor draft, quoting the new text, and noting that the rest of the page is untouched.
With the editor open, the edit lands in your draft and is highlighted in place: the intro paragraph rewritten, the rest of the page untouched, the commit still yours to make.

Securing an agent that can edit your docs

Fern Agent never runs with a privileged service identity. Whether you reach it through the Dashboard, an MCP client, or Slack, the turn executes as you: your Auth token is carried through to every internal and external service the agent touches. Because every request runs with your token, the agent automatically respects RBAC and multi-tenancy. It can only read the docs sites your org owns, and search results are scoped to your roles: an admin sees admin-only content, a developer sees developer content, and an anonymous caller sees only what's public.

Everything the agent reads is screened or labeled. Fern treats anything the agent didn't write as potentially hostile. Your request is screened on the way in, and any content the agent pulls in during a turn (a fetched web page, user feedback, reviewer comments) is passed through a dedicated screening guardrail before the model sees it. Clean content arrives explicitly labeled as untrusted data, not instructions; content that tries to hijack the agent is withheld, down to the individual record where possible so a single poisoned row doesn't blackhole a whole result set. Screening is chunked so an injection buried deep in a large page is still caught.

Who built it

We built Fern Agent together. The shared decisions came first:

  • Putting the agent in the Python backend
  • Refactoring routes into services
  • The auth model and the prompt-injection guardrail
  • Building one toolset for our agent and yours

From there, Clay led the hosted agent, MCP, and Slack. Mathieu led design and UX across the Dashboard, Slack, and the visual editor.

That design work started as one mega pull request: a working prototype of the whole experience, built directly in the Dashboard instead of in Figma. Static mocks couldn't be pointed at a real docs site or show what a streaming turn feels like. The coded prototype could, so we demoed it against real docs before chunking it into individual features and rebuilding them properly on top of the backend work.

Devin authored 87 of this project's 93 commits, and every one of them was reviewed by one of us before it landed. Devin was good at the middle of the work, turning a decided design into a lot of correct, consistent code across a large surface. It was not deciding what to build, or whether the result was safe to ship.

Open your Dashboard

Fern Agent is live now. Open your Dashboard and ask it to review and fix your underperforming pages. Mention it in Slack, or run claude mcp add and close the loop from your own agent.

New to Fern? Try it for free or book a demo to see the agent on your docs first.

Clay TondreauMathieu Legault