Build agents / Definitions & instances

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

DefinitionInstance
Lives inagents/<route>/agent.tsYour FoundryDataAdapter
IdentityThe file path: agents/support/agent.ts is supportAn id string such as support-acme
HoldsModel, prompt, tools, assembly logic, the capability catalogueContext, installations, playbooks, workspace
Changes byDeploying codeconfigureAgent, install/uninstall, provisioning
How manyOne per routeAny 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:

agents/support/agent.ts (excerpt)typescript
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

FieldTypeNotes
idstringYours, or agent_<uuid> when omitted. Unique per data adapter.
definitionIdstringThe definition route, e.g. support. Fixed for the instance's life.
workspaceIdstringDefault "default". Its conversations and requests must use the same workspace.
provisioningKey?stringSet only by event-driven provisioning; the data adapter keeps it unique.
contextRecord<string, unknown>Free-form JSON. Resolvers read it as ctx.agentInstance.context.
installationsAgentInstallation[]{ kind: "tool" | "application" | "mcp", id, config?, accountId? }
playbooksAgentPlaybook[]Inbound-event policy (see Playbooks, schedules & sleep).
createdAt, updatedAtISO-8601 stringupdatedAt 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

WhereAPI
Code, loaded at startupdefineAgentInstance(definition, options) in a data adapter's agents seed list
Your server processruntime.createAgent("support", options) on a FoundryRuntime
Typed clientfoundry.agent("support").create(options) or foundry.createAgent("support", options)
HTTPPOST /api/agent-instances with { definitionId, ...options } → 201 and the record
InspectorNew run can create an instance in the current or default workspace
Inbound eventsPlaybook 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.

agents/support/instances.tstypescript
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",
});
foundry.application.tstypescript
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],
  }),
});
  • definitionId is read from the imported definition's file identity, which Foundry binds during discovery. Pass seeds to an adapter; do not serialize them yourself.
  • MemoryFoundryDataAdapter accepts the same agents, conversations, subscriptions, activations and environment seeds. It reloads them on every start and loses everything else at restart.
  • FileFoundryDataAdapter writes 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; use configureAgent, 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

scripts/onboard.tstypescript
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);
HTTPhttp
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. context is not merged; spread the current value yourself.
  • Fields you omit are kept. id, definitionId, workspaceId and provisioningKey cannot 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: setAgentPlaybooks or PUT /api/agent-instances/:id/playbooks.
  • There is no API to delete an instance.
scripts/disable-refunds.tstypescript
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
});
HTTPhttp
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:

  1. A message arrives: foundry.send, POST /api/conversations/:id/messages, an OpenAI-compatible request, a schedule, a sleep wake-up, or a transmission.
  2. 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 definitionId matches the route.
  3. That snapshot — request, instance, conversation and the instance's activations — is handed to a fresh run process.
  4. 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 configureAgent or 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

SituationReferenceWhy
A definition imports another static primitiveImported value (install(helpdesk))Renames fail at compile time and editors follow the reference.
A typed client selects an agent routeGenerated string union (agent("support"))Serialized over HTTP, but checked against discovered files.
A client or HTTP call installs a capability{ kind, id } stringsFile ids are bound by discovery inside Foundry; outside it, install(def) cannot read an id.
A persisted instance points to its definitiondefinitionId stringData must survive processes and deployments.
A run targets an instance or conversationRuntime idsThese 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.