Start / Agents are data

Agents are data

defineAgent does not define an agent. It defines everything a kind of agent could be: the tools, apps, memory, prompt and rules it can draw on. The agents themselves are instances, and they are data. Their names, jobs, apps, owners and teammates are data too. Get this right and adding an agent is a write to a database. Get it wrong and it is a deploy.

This is how Foundry has always worked: a definition is code, an instance is a record, and defineAgentInstance describes one in code by pointing at the imported definition. What is easy to get wrong is how far it goes.

A page and its props

Think of a server-rendered page in Next.js. The page is code: it lays out what a profile can show and what it can do. Each profile is data passed in as props. Twitter does not have a page in its codebase for every user. When someone changes their name, nobody redeploys the site.

Foundry works the same way. agents/bot/agent.ts is the page. Each bot a person creates (“Sales Outbound”, “Inbox Manager”, “Bug Triage”) is an instance of it, and none of them is above the others. The definition reads the instance and assembles the right agent for every run.

agents/bot/agent.ts (excerpt)typescript
export default defineAgent({
  description: "A bot: an AI teammate with its own job, apps and memory",
  components: composeAgent(gmail, sheets, tickets, xApp, memory), // what a bot CAN have
  systemPrompt: (_agent, ctx) => botPrompt(ctx),                  // built from the bot's data
  tools: (_agent, ctx) => botTools(ctx),
  model: botModel,                                                // chosen from the bot's data
  mesh: { waitMs: 60_000 },                                       // bots message each other by id
});

A person's agents are records. Describe them as plain data, then either seed them or create them when the person signs up:

lib/people.tstypescript
export function botFor(person: Person, bot: { id: string; name?: string; job?: string }) {
  return {
    id: bot.id,
    workspaceId: person.id,                 // her team
    name: bot.name ?? "New bot",            // who it is
    ...(bot.job ? { description: bot.job } : {}),
    context: { owner: person.name },
  };
}

// Her connected apps are installations too: the Gmail app with her account.
export function installationFor(account: ConnectedAccount): AgentInstallation {
  return { kind: "application", id: APPS[account.app].id, accountId: account.id };
}
foundry.application.ts: the demo account, seededtypescript
import bot from "./agents/bot/agent.js";

const mayaGmail = await readAccount("maya-gmail");             // from your account store
const inbox = defineAgentInstance(bot, botFor(maya, {
  id: "maya-inbox",
  name: "Inbox Manager",
  job: "Keeps Maya's inbox moving.",
  routines: [{ on: "new-mail", instruction: "Answer what you can; hand the rest on." }],
  installations: [installationFor(mayaGmail)],               // her Gmail, with her account
}));                                                         // the definition is imported, not named

export const data = new FileFoundryDataAdapter({
  file: ".data/foundry.json",
  agents: [inbox],                                           // written only if the id is missing
  conversations: [createConversation(inbox, chatWith(inbox.id))],
});
your app: when someone makes a bottypescript
await foundry.createAgent("bot", botFor(maya, { id: "maya-bot-4", name: "Inbox Manager" }));
await giveAccounts(foundry, "maya-bot-4", maya);             // install each app she connected, with her account

// Later, a rename is one more write. No deploy.
await foundry.configureAgent("maya-bot-4", { name: "Inbox Zero" });

Over HTTP the kind of agent is its route, because a request crosses a process boundary. Inside the project, refer to the definition by importing it.

What goes where

In the definition (code)On the instance (data)
Which tools, apps and MCP servers a kind of agent may useWhich ones this agent has installed
How the prompt is builtWhat goes into it: its name, its job, its owner, its rules
How the model is chosenThe facts it is chosen from, such as a role or a plan
How schedules are computedThe time zone, the brief time, the routines it was taught
Whether it is on the mesh, and the rule for who it may messageWho is actually in its workspace, and the data the rule reads
Memory, workspace and store adaptersWhat it has remembered and the files it has written
The shape of a group chatWhich agents are in this one: conversation.context

Two more pieces of data hold this up. A workspace is a tenant: a person's or a company's agents live in one and only see each other. A conversation can carry its own data too, such as the members of a group chat or the channel it stands for.

The mistakes it rules out

Each of these has been built for real, by people and by coding agents. They all come from treating a definition as an agent.

MistakeWhat happensInstead
A definition per agent: agents/sales-outbound/, agents/inbox-manager/A person cannot make a bot. Every new one is a code change and a deploy.One bot definition; each bot an instance.
A name or a job in code: systemPrompt: "You are Sales Outbound"Renaming means redeploying, and two people cannot both have a sales bot with their own name.name and description on the instance; the prompt reads them.
Teammates by route: an agent that can only reach “the coder definition”Foundry has to invent a worker per route, so the agent you made is not the one that answers.Agents message each other by instance id over the mesh; who is on the team is data.
Branches on identity: if (agentId === "maya-inbox")Behaviour depends on ids baked into code, and breaks for the next person.Branch on data the instance carries: context.routines includes new mail.
Per-user settings in environment variables or constantsOne deployment can only serve one person.Put them in context, read them in resolvers, change them with configureAgent.

When a second definition is right

Split definitions by what a kind of agent can do and what it needs to load, never by who it is. A new definition earns its place when:

  • It runs differently: a voice front with a speech model and a run of its own, next to a text agent.
  • It loads different things: a coding worker that pulls in a sandbox SDK you do not want in the chat agent's process.
  • Its tools must never mix: a read-only research agent that should have no way to act at all.

Even then, every agent of that kind is still an instance, created by your application with its own name and data.

Agents can change their own data

Because identity is data, an agent can change it with a tool. A new bot that is told its job names itself:

agents/bot/tools.ts (excerpt)typescript
{
  name: "take_job",
  description: "Take on the job your owner described: give yourself a short name and one sentence saying what you do.",
  inputSchema: z.object({ name: z.string(), description: z.string() }),
  async do(job) {
    const current = await Effect.runPromise(ctx.data.getAgent(ctx.agentId));
    await Effect.runPromise(ctx.data.putAgent({ ...current!, ...job, updatedAt: new Date().toISOString() }));
    return { status: "success", data: { renamed: job.name } };
  },
}

Its peers see the new name the next time they list who is on the team. Skills it is taught can go in the workspace, so every agent on the team reads the same library.

See it built