Multi-agent workforce
A workforce is many harnesses working on one outcome: a lead that splits the work, specialists that each own a part, and a record of who is doing what. The hard parts are not the agents. They are starting the right agents when work arrives, handing results between runs that do not overlap in time, and seeing what happened afterwards. Foundry has a primitive for each, so the workforce is mostly prompts and definitions.
- One definition per role, each with its own prompt and the fewest tools that role needs
- The lead's prompt: who exists, how work is handed out, what done looks like
- An inboxes resolver that hands each result to the conversation waiting for it
- A mesh transport (Redis, NATS…) if peers must talk while both are running
Pick the smallest boundary for each hand-off
Most multi-agent designs fail by using one mechanism for everything. Each hand-off in a workforce has a shape, and Foundry has a primitive for each shape:
| The hand-off | Use | The result comes back |
|---|---|---|
| A bounded sub-task the lead waits for, with fewer tools | defineSubagent | As the lead's tool result, in the same run |
| Separate durable work that should run on its own | glove_foundry_spawn | Through the shared inbox, a task or a workspace entry |
| Peers that must talk while both are working | mesh | In the receiver's inbox, on its next turn |
| A result that may outlive any one run | Workspace entries, tasks, the shared inbox | Whenever someone reads it |
| Work arriving from outside should start a team | A playbook subscription | Provisioned instances, each with a run |
| Fan-out and fan-in that your code controls | The typed client | handle.wait() |
How it fits together
A brief arrives through a transmission. A subscription matches it and provisions a lead instance for that brief (provisioning: { mode: "per-thread" }), so two briefs never share a lead. The lead's first run writes a plan as tasks with glove_foundry_tasks, then calls glove_foundry_spawn once per specialist: a researcher and a writer, each a new instance with its own conversation, queued at once. The lead's run ends; it does not wait.
The researcher saves its findings to a file and posts a shared inbox item addressed to the lead's conversation with the path, not the body. When the lead runs next (on the writer's report, or on a schedule that checks progress), its inboxes resolver loads the items posted to it and Glove injects them into that turn. Tasks move from open to completed as each part lands. Anything that publishes is permission-gated, so the final step waits on an approval. The inspector shows the whole tree: the arrival, the provisioned lead, every spawned run, each model and tool call, and the approval.
Skeleton
lead/agent.ts plans, spawns specialists, assembles, publishes (gated)
lead/subscriptions/brief.subscription.ts one lead per incoming brief
researcher/agent.ts sources and numbers; writes files, reports by shared inbox
writer/agent.ts drafts from the researcher's filesimport type { InboxItem } from "glove-core";
import { createAdapter } from "glove-core/models/providers";
import { defineAgent, defineSubagent } from "glove-foundry";
import { Effect } from "effect";
import { z } from "zod";
import { FileStore } from "../../lib/file-store.js";
import { publish } from "../../lib/publish.js"; // your code: where finished work goes
export default defineAgent({
description: "Leads one brief: plans it, hands out the parts, assembles the result",
model: () => createAdapter({ provider: "anthropic", model: "claude-sonnet-5-5" }),
store: ({ conversationId }) => FileStore.open(conversationId),
systemPrompt: (_agent, ctx) => `You lead the team for one brief (conversation ${ctx.conversationId}).
1. Write the plan as tasks with glove_foundry_tasks.
2. Start specialists with glove_foundry_spawn: definitionId "researcher" for sources and numbers,
"writer" for drafts. Give each the conversation id above so it can report back.
3. Specialists post results to the shared inbox as file paths. Never paste documents.
4. When every task is completed, assemble and call publish.`,
// Hand each shared-inbox item posted to this conversation to the model, once.
// glove_foundry_shared_inbox "post" creates items as "pending".
inboxes: async (_agent, ctx) => {
const items = await Effect.runPromise(ctx.data.listInboxItems(ctx.workspaceId));
const ready = items.filter((item) => item.conversationId === ctx.conversationId && item.status === "pending");
for (const item of ready) {
await Effect.runPromise(ctx.data.putInboxItem({ ...item, status: "dismissed", updatedAt: new Date().toISOString() }));
}
return ready.map((item): InboxItem => ({
id: item.id,
tag: item.topic,
request: `Result: ${item.topic}`,
response: JSON.stringify(item.payload).slice(0, 4_000),
status: "resolved",
blocking: false,
created_at: item.createdAt,
resolved_at: item.updatedAt,
}));
},
tools: [
{
name: "publish",
description: "Publish the finished deliverable.",
inputSchema: z.object({ path: z.string() }),
requiresPermission: true, // a person signs off in the inspector or your UI
async do({ path }) {
return { status: "success" as const, data: await publish(path) };
},
},
],
subagents: [
defineSubagent({
name: "editor",
description: "Review a draft for gaps and contradictions. Read-only; returns notes.",
systemPrompt: "Review the draft you are given. Return a short list of concrete problems.",
}),
],
});A specialist reports back with the same built-in tools every Foundry agent has. Its prompt tells it to finish with glove_foundry_shared_inbox({ action: "post", topic, payload, conversationId }) addressed to the lead's conversation, and to mark its task completed with glove_foundry_tasks. The subscription that starts a lead per brief follows the urgent-ticket example with provisioning: { mode: "per-thread" }.
Each Foundry run is its own process, so an in-memory mesh cannot connect two runs. When peers must talk while both are running, give the mesh field a transport-backed adapter, or build the whole team inside one run as braind-storm does. Files several instances share need a shared backend too: give each working environment the same cachedRemote VFS over a bucket.
What not to build
- An orchestrator service that creates agents for incoming work. Subscriptions provision zero, one or many instances per event, atomically and idempotently.
- A job queue between agents.
glove_foundry_spawnqueues a durable run on another instance and returns at once. - Coordination tables. Workspace entries, tasks and the shared inbox are already persisted, and
compareAndSetWorkspaceEntrygives you an atomic claim so only one agent takes a job. - A message bus for live peers. Mesh delivers into inboxes, with blocking sends and acknowledgements, over a transport you supply.
- A dashboard to see what the team did. The inspector correlates every run by definition, instance, conversation and run, and steering redirects work in place.
Deploy on Kuiper
- Deploy the Foundry app as one unit. Every definition and instance runs inside it; Kuiper keeps one replica of a unit with a volume, which is what one runtime per data store needs (two runtimes on one store would both fire its schedules).
- Keep the data adapter, transcripts and memory on
/data. When the team's files outgrow the volume or must be shared, put them in a Kuiper bucket behind acachedRemoteVFS; its credentials arrive as variables. - Route every role's model through the gateway.
kuiper models usage --by unitshows what the workforce costs, and a budget can notify or block before it surprises you. - Watch it from the project's Agents page: instances, runs and failures for every definition, read from the app's own API.
Full walkthrough: Deploy on Kuiper.
Go further
- examples/foundry-braind-storm: a brand workforce of five agents over mesh, with a shared working environment, image generation, parallel campaigns and a live observatory.
- Build the parts one at a time: split the work, make it a system.
- Reference: Multi-agent systems, Conversations & shared work, Playbooks, schedules & sleep, Mesh.