Back to blog
Guides7 min read

Defining a Vercel Eve agent: the folder is the configuration

How Eve's filesystem-first layout works — why a tool's filename is the name the model calls, what a subagent does and does not inherit, and which decisions are worth making before the first deploy.

There is no manifest

Most agent frameworks ask you to register things. You write a tool, then you tell a builder about it; you write a prompt, then you pass it to a constructor. The registration is where the framework learns what exists, and it is also where the two halves drift apart.

Eve, Vercel's framework for durable backend agents, does not have that step. It scans one directory and derives everything from paths:

my-agent/
  agent/
    instructions.md      ← the system prompt (required)
    agent.ts             ← model, reasoning, limits (optional)
    tools/               ← one file per tool
    skills/              ← markdown loaded on demand
    subagents/           ← child agents, one directory each
    schedules/           ← cron-triggered prompts
    connections/         ← MCP and OpenAPI services
    hooks/               ← subscribers on the runtime event stream
    sandbox/             ← the isolated shell and its network policy
  evals/                 ← scored checks, outside agent/

agent/tools/get_weather.ts is the tool get_weather. agent/skills/summarize.md is the skill summarize. agent/subagents/researcher/ is the subagent researcher. The slot a file lands in decides how it loads, and the filename decides what it is called.

The free Eve agent generator builds that folder from a form, which is a fast way to see the shape before writing any of it by hand.

The filename is a load-bearing decision

This is the part that catches people coming from a register-it framework: there is no name field inside a tool file to correct a bad filename with. The stem is the name, and the name is what the model emits when it decides to call the thing.

So agent/tools/Get Weather.ts is not a style problem. Lowercase, digits and underscores, starting with a letter — get_weather, not Get Weather, not getWeather. Rename the file and you have renamed the tool for the model, which means any eval, any prompt and any skill that mentioned the old name is now wrong.

A tool file itself is small:

import { defineTool } from "eve/tools"
import { z } from "zod"

export default defineTool({
  description: "Look up one order by its id and return its status.",
  inputSchema: z.object({
    orderId: z.string().describe("The id, as printed on the confirmation email."),
  }),
  async execute({ orderId }) {
    // …
  },
})

Two fields do more work than they look like they do. The description is the entire basis on which the model decides to call this tool rather than another one — a tool with a vague description is called at the wrong times, and one with no description is barely called at all. And every .describe() on a parameter is the only thing telling the model what belongs in that field; the model never sees this file, only the schema derived from it.

For anything that spends money, moves data or talks to a customer, add an approval gate:

import { always } from "eve/tools/approval"

always() stops the call until a human approves it, once() asks the first time. This is cheap to add on day one and awkward to retrofit after an agent has been running unattended for a week.

Skills are loaded, not included

A skill is a markdown playbook in agent/skills/. The important property is that its body is not in the context window until the model asks for it — it calls load_skill, and only then does the procedure get appended to the turn.

What routes that decision is the description in the frontmatter:

---
description: Follow the refund policy before promising money back.
---

1. Check the order is inside the return window.
2. …

Which means a skill with a good body and a vague description is dead weight: nothing ever matches it, so nothing ever loads it. Write the description as the condition under which the skill applies, not as a summary of what it contains.

A subagent inherits nothing

This is the single most surprising rule in Eve, and the one worth internalising before you build a multi-agent anything.

A declared subagent under agent/subagents/ receives only what is authored in its own directory, plus framework defaults. Not the root's model. Not the root's tools. Not the root's skills. A subagent directory is the root agent's shape one level down:

agent/subagents/researcher/
  agent.ts             ← description (required) + its own model
  instructions.md
  tools/               ← its own
  skills/              ← its own

Two consequences. First, a subagent with an empty tools/ can do nothing but write prose back to whoever called it. That is a perfectly good thing to want — research, judgement calls, second opinions — but it is not what most people mean when they say "delegate this".

Second, the description in its agent.ts is mandatory; Eve's compiler rejects a subagent without one. That is not bureaucracy. The description is the only thing the parent reads when deciding whether to delegate, so an absent one makes the subagent invisible in practice as well as invalid at build time.

And the delegation itself is lossy by design: a child never sees the parent's conversation. It is called with a single message. Every fact it needs — what the customer said three turns ago, which account this is about — has to be restated in that message. If your subagent keeps answering as though it lacks context, this is why, and the fix is in the parent's instructions rather than the child's.

The decisions that cost money while nobody is watching

A durable agent is not a request-response function. It can be woken by a schedule or a channel and keep working for days, which changes which defaults matter.

Schedules are a cron string and a prompt:

import { defineSchedule } from "eve/schedules"

export default defineSchedule({
  cron: "0 9 * * 1",
  markdown: "Summarise last week's failed payments and post the list.",
})

Five fields, and a malformed one deploys as a job that silently never fires. Worth validating before it ships rather than noticing in three weeks.

Limits are generous by default — Eve allows a very large input budget and a 30-day session. That is the right default for a framework and the wrong one for a first deploy, because a loop is expensive long before it is visible. Set maxInputTokensPerSession and a session timeout you would actually accept.

Sandbox egress is the one to think hardest about if the agent runs model-authored code. The default allows all network access, which means the sandbox can reach anything its credentials can. deny-all blocks everything including DNS; an allow-list names the hosts:

await use({ networkPolicy: { allow: ["ai-gateway.vercel.sh", "*.github.com"] } })

Credentials stay out of the model

A connection mounts somebody else's tool surface — an MCP server, an OpenAPI service — under agent/connections/, and its tools reach the model prefixed: linear__list_issues.

import { defineMcpClientConnection } from "eve/connections"

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace.",
  auth: { getToken: async () => ({ token: process.env.LINEAR_API_TOKEN! }) },
})

The token is fetched inside getToken, which keeps it step-local and out of the model's context entirely. Connections also take an approval gate, and there is a decent argument for gating one you would not gate on a local tool: you did not write that tool surface, and it can change under you between deploys.

Prove it, then ship it

Evals live outside agent/, in an evals/ directory, because a test is not something the agent can do:

import { defineEval } from "eve/evals"
import { includes } from "eve/evals/expect"

export default defineEval({
  description: "Looks an order up instead of guessing.",
  async test(t) {
    await t.send("Where is order 10432?")
    t.succeeded()
    t.calledTool("lookup_order")
    t.check(t.reply, includes("delivered"))
  },
})

t.calledTool is the assertion that catches the failure mode specific to agents — a confident, well-written, entirely invented answer. Run them with eve eval.

One last thing that is not a file: channels are scaffolded by the CLI. eve add channel/slack writes agent/channels/slack.ts, installs the dependency and adds the credentials it needs to your .env.example. Hand-writing that file is guessing at three moving targets at once.

Where to start

Node 24 or newer, then:

npx eve@latest init my-agent
cd my-agent
npm run dev

That gives you the terminal UI against a local agent. If you would rather see the whole folder before you commit to it — tools with their schemas, a subagent with its own tools, the sandbox policy, the evals — the Eve agent generator produces it as a downloadable .zip, free and without an account.

Designing the app the agent works for is the other half of the problem. Nodlume's structure painter sketches a Next.js route tree and exports a real App Router skeleton, and the free tools cover theming, data models and permissions for the targets around it.

Target-specific scaffolding

Continue this learning path

Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.

Explore platform generators

Design your application

Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.

Open projects

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.