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.
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:
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 };
}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))],
});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 use | Which ones this agent has installed |
| How the prompt is built | What goes into it: its name, its job, its owner, its rules |
| How the model is chosen | The facts it is chosen from, such as a role or a plan |
| How schedules are computed | The time zone, the brief time, the routines it was taught |
| Whether it is on the mesh, and the rule for who it may message | Who is actually in its workspace, and the data the rule reads |
| Memory, workspace and store adapters | What it has remembered and the files it has written |
| The shape of a group chat | Which 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.
| Mistake | What happens | Instead |
|---|---|---|
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 constants | One 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
runof 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:
{
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
- Rebuild Grok Bot: one bot definition; every bot, its name, its apps and its group chats are data.
- Rebuild Claude Duet: one partner per person, with its schedule built from the person's data.
- Rebuild OpenAI Dots: one definition; each dot's responsibility and rules are its data.
- Definitions & instances: the record, and every way to create and change one.