Definitions and instances
A definition is code that describes a kind of agent. An instance is one durable agent built from it, with its own id, workspace, context, installed capabilities and playbooks. You serve a new customer by creating data, not by writing code, and every run rebuilds the agent from the latest code and the latest data.
What lives where
| Definition | Instance | |
|---|---|---|
| Lives in | agents/<route>/agent.ts | Your FoundryDataAdapter |
| Identity | The file path: agents/support/agent.ts is support | An id string such as support-acme |
| Holds | Model, prompt, tools, assembly logic, the capability catalogue | Context, installations, playbooks, workspace |
| Changes by | Deploying code | configureAgent, install/uninstall, provisioning |
| How many | One per route | Any number per definition |
Conversations belong to an instance (one instance can own many) and runs belong to a conversation; see Conversations.
Definitions live in code
agents/support/agent.ts is discovered as the support route. You never repeat that identity inside defineAgent, and the generated .foundry/routes.d.ts gives clients a closed, type-checked set of routes. Colocated primitives follow the same rule: a tool, app, MCP server, memory profile, layer or subscriber gets its id from its file name during discovery. In code you import the value itself — install(helpdesk), not install("helpdesk") — so renames fail at compile time.
A definition reads instance data through its resolvers. This one prompt serves every tenant:
systemPrompt: (_agent, { agentInstance }) =>
`You are the support agent for ${String(agentInstance.context.tenant ?? "our store")}.`,All fields and the assembly context are listed in defineAgent & assembly.
The AgentInstance record
| Field | Type | Notes |
|---|---|---|
id | string | Yours, or agent_<uuid> when omitted. Unique per data adapter. |
definitionId | string | The definition route, e.g. support. Fixed for the instance's life. |
workspaceId | string | Default "default". Its conversations and requests must use the same workspace. |
provisioningKey? | string | Set only by event-driven provisioning; the data adapter keeps it unique. |
context | Record<string, unknown> | Free-form JSON. Resolvers read it as ctx.agentInstance.context. |
installations | AgentInstallation[] | { kind: "tool" | "application" | "mcp", id, config?, accountId? } |
playbooks | AgentPlaybook[] | Inbound-event policy (see Playbooks, schedules & sleep). |
createdAt, updatedAt | ISO-8601 string | updatedAt changes on every configure/install. |
Foundry freezes the record it reads back and deep-copies context and installation config, so a resolver cannot mutate persisted data by accident. Keep context JSON-serializable and free of secrets: it is returned by the API and shown in the inspector.
Creating instances
| Where | API |
|---|---|
| Code, loaded at startup | defineAgentInstance(definition, options) in a data adapter's agents seed list |
| Your server process | runtime.createAgent("support", options) on a FoundryRuntime |
| Typed client | foundry.agent("support").create(options) or foundry.createAgent("support", options) |
| HTTP | POST /api/agent-instances with { definitionId, ...options } → 201 and the record |
| Inspector | New run can create an instance in the current or default workspace |
| Inbound events | Playbook subscriptions provision instances (below) |
options is { id?, workspaceId?, context?, installations?, playbooks? }. Omitted fields default to agent_<uuid>, "default", {}, [] and []. There is no provisioningKey option; only provisioning sets it. createAgent rejects an unknown definition, an installation that is not in that definition's components, invalid playbooks, and an id that already exists.
Seeding instances from code
defineAgentInstance returns a plain AgentInstance value and registers nothing. It becomes real when you hand it to the data adapter in foundry.application.ts. Pair it with createConversation so the instance has a thread to talk in.
import { createConversation, defineAgentInstance, install } from "glove-foundry";
import support from "./agent.js";
import helpdesk from "./apps/helpdesk.app.js";
import lookupOrder from "./tools/lookup-order.tool.js";
export const acmeSupport = defineAgentInstance(support, {
id: "support-acme",
workspaceId: "acme",
context: { tenant: "Acme", canRefund: true },
installations: [
install(lookupOrder, { region: "eu" }),
install(helpdesk, { queue: "billing" }),
],
});
export const acmeMain = createConversation(acmeSupport, {
id: "support-acme-main",
title: "Acme support",
});import { FileFoundryDataAdapter, defineApplication } from "glove-foundry";
import { acmeMain, acmeSupport } from "./agents/support/instances.js";
export default defineApplication({
name: "Support",
data: new FileFoundryDataAdapter({
file: ".foundry/data/state.json",
agents: [acmeSupport],
conversations: [acmeMain],
}),
});definitionIdis read from the imported definition's file identity, which Foundry binds during discovery. Pass seeds to an adapter; do not serialize them yourself.MemoryFoundryDataAdapteraccepts the sameagents,conversations,subscriptions,activationsandenvironmentseeds. It reloads them on every start and loses everything else at restart.FileFoundryDataAdapterwrites a seed only when no record with that id exists yet. Editing a seed later does not change an instance already in the state file; useconfigureAgent, or delete the file in development.- At startup Foundry validates every stored instance. One that names an unknown definition, or installs something missing from its definition's
components, stops the runtime from starting.
Adapters and durability are covered in Persistence.
Creating at runtime
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "../.foundry/routes.js";
const foundry = createFoundryClient<FoundryRoutes>({ baseUrl: "http://127.0.0.1:4141" });
const agent = await foundry.agent("support").create({
id: "support-globex",
workspaceId: "globex",
context: { tenant: "Globex", canRefund: false },
installations: [{ kind: "tool", id: "lookup-order", config: { region: "us" } }],
});
const conversation = await foundry.createConversation(agent.id, { title: "Globex support" });
const run = await foundry.send(agent.id, conversation.id, "Where is order A-1001?");
console.log((await run.wait()).output);POST /api/agent-instances
content-type: application/json
{
"definitionId": "support",
"id": "support-globex",
"workspaceId": "globex",
"context": { "tenant": "Globex" },
"installations": [{ "kind": "tool", "id": "lookup-order", "config": { "region": "us" } }]
}Changing an instance
configureAgent(agentId, { context?, installations?, playbooks? }) exists on the runtime and the client, and as PATCH /api/agent-instances/:id.
- Each field you pass replaces that whole field.
contextis not merged; spread the current value yourself. - Fields you omit are kept.
id,definitionId,workspaceIdandprovisioningKeycannot be changed; create a new instance instead. - Installations and playbooks are validated exactly as in
createAgent. - To add or remove one capability, use
install/uninstall(PUT/DELETE /api/installations); see Installing on an instance. Playbooks alone:setAgentPlaybooksorPUT /api/agent-instances/:id/playbooks. - There is no API to delete an instance.
import { createFoundryClient } from "glove-foundry/client";
const foundry = createFoundryClient({ baseUrl: "http://127.0.0.1:4141" });
const current = (await foundry.agentInstances("support")).find((a) => a.id === "support-acme");
if (!current) throw new Error("support-acme not found");
await foundry.configureAgent(current.id, {
context: { ...current.context, canRefund: false }, // replace, so carry the rest over
});PATCH /api/agent-instances/support-acme
content-type: application/json
{ "context": { "tenant": "Acme", "canRefund": false } }Every run rebuilds the agent
Nothing about an assembled agent is cached between runs. For each inbound message:
- A message arrives:
foundry.send,POST /api/conversations/:id/messages, an OpenAI-compatible request, a schedule, a sleep wake-up, or a transmission. - The host loads the instance and conversation from the data adapter and checks that the conversation belongs to the instance, that both share the request's workspace, and that the instance's
definitionIdmatches the route. - That snapshot — request, instance, conversation and the instance's activations — is handed to a fresh run process.
- The run loads the current definition code and re-runs every lazy resolver against the new message, then mounts the instance's installations (full order).
- A
configureAgentor install change applies from the next run, with no restart. A run already in progress keeps the snapshot it started with. - New definition code applies to every existing instance on its next run; instance data is untouched.
- History comes from the conversation store, not from the instance; see Conversations.
Where strings are correct
| Situation | Reference | Why |
|---|---|---|
| A definition imports another static primitive | Imported value (install(helpdesk)) | Renames fail at compile time and editors follow the reference. |
| A typed client selects an agent route | Generated string union (agent("support")) | Serialized over HTTP, but checked against discovered files. |
| A client or HTTP call installs a capability | { kind, id } strings | File ids are bound by discovery inside Foundry; outside it, install(def) cannot read an id. |
| A persisted instance points to its definition | definitionId string | Data must survive processes and deployments. |
| A run targets an instance or conversation | Runtime ids | These identities are created and updated dynamically. |
Instances provisioned by events
You do not have to create every instance ahead of time. A playbook subscription listens for an inbound transmission and targets a definition with a provisioning mode: singleton, per-thread, per-event, existing (listed instance ids) or custom (your application's provisioner). When a matching event arrives and the target instance does not exist, Foundry calls the data adapter's provisionAgent, which creates the instance atomically under a provisioningKey (an existing key returns the existing instance), creates or reuses the conversation for that thread, and starts the run. Inbound deliveries are claimed by route and event id, so a retried delivery does not create duplicate instances or runs. See Playbooks, schedules & sleep.