Build it up · Step 7 of 8

Make it a system

The release assistant is a complete harness, but it is still one agent in one process. Every team wants their own. It should check releases every Monday without being asked. A Slack message should reach it. Publishing should wait for a person's approval even when nobody is watching the chat, and the rest of your stack should be able to call it. That is the line between a harness and a system, and it is what Glove Foundry is for.

Outside worldSlack · email · webhooksTransmissionverify · normalisePlaybookmatch · provisionSchedulescron · interval · sleepInstancedefinition + dataClienttyped · REST · OpenAIConversationdurable threadRuna fresh Glove per runReplyoutbound toolApprovalpause · approve · denyInspectorevery run and calleventtypedroutes · createsactivationmessagehistoryassemblesrepliesasksemits
How work reaches a Foundry agent and what comes out. Events from the outside world pass through a transmission and a playbook, which may create the instance that handles them; schedules wake an instance on their own; clients send messages into a conversation. Every run assembles a fresh Glove from the definition, the instance and the message, in its own process.

Move the harness into Foundry

terminalbash
npx glove-foundry init release-desk --template minimal
cd release-desk && pnpm install && pnpm dev     # inspector and WebChat at http://localhost:4141

A Foundry project is a folder of agent definitions. The file path is the agent's id, so agents/release/agent.ts is the agent release. Everything from steps 1 to 6 moves into its fields:

agents/release/agent.tstypescript
import { createAdapter } from "glove-core/models/providers";
import { defineAgent, defineSchedule } from "glove-foundry";
import { FileStore } from "../../lib/file-store.js";
import { listMergedChanges, tagRelease } from "./tools.js";   // server-side versions of the step 1 and 2 tools

const mondayCheck = defineSchedule({
  name: "monday-release-check",
  timing: { kind: "cron", expression: "0 9 * * 1", timezone: "UTC" },
  message: "Check what merged last week and say whether a release is due.",
});

export default defineAgent({
  description: "Drafts, reviews and ships releases for one team",

  // Every field is a value or a function of the run's context.
  model: () => createAdapter({ provider: "anthropic", model: "claude-sonnet-4-20250514" }),
  systemPrompt: (_agent, ctx) =>
    `You help the ${String(ctx.agentInstance.context.team)} team ship releases. Use tools for facts.`,

  // One durable store per conversation (step 4): each run is a fresh process.
  store: ({ conversationId }) => FileStore.open(conversationId),

  // Always-on tools. On the server, tag_release drops its pushAndWait confirm and
  // calls your release API directly with requiresPermission: true, which Foundry
  // turns into an approval record (below).
  tools: [listMergedChanges, tagRelease],

  // Desired recurring work, per instance; a team can opt out in its context.
  schedules: (_agent, ctx) => (ctx.agentInstance.context.weekly === false ? [] : [mondayCheck]),

  compactionInstructions: () => "Keep what is merged, what is decided and what is blocked.",
});

The tools move to the server: tag_release calls your release API directly instead of fetch("/api/releases"), and its browser confirm becomes requiresPermission: true. Context providers, skills, memory, the working environment, a REPL and subagents have fields of their own (contextProviders, skills, memory, workingEnvironment, repl, subagents) or mount in configure(agent, ctx). See defineAgent & assembly.

Identity: one definition, an instance per team

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 payments = await foundry.agent("release").create({ context: { team: "payments" } });
const thread = await foundry.createConversation(payments.id, { title: "2.4.0" });

const run = await foundry.send(payments.id, thread.id, "Draft notes for 2.4.0 and publish when ready.");
const done = await run.wait({ timeoutMs: 120_000 });

An instance is data: its id, its context, the capabilities installed on it, its playbooks. Each has any number of conversations. Every run reloads the instance and assembles a fresh harness from the definition, the instance and the message, in its own process. That is also why the store in step 4 had to be durable: a MemoryStore would start every message empty.

Time: schedules, sleep, spawn

The Monday schedule above is reconciled into a durable activation for every instance and rebuilt on restart. Agents also get tools to make their own: glove_foundry_schedule ("remind me Thursday"), glove_foundry_sleep ("check CI again in 20 minutes": the run suspends and the same conversation wakes later), glove_foundry_schedules to list, pause or cancel them, and glove_foundry_spawn to start another instance's run. No job queue, no cron.

Oversight: approvals that wait for a person

In step 2, requiresPermission showed a prompt in the browser. In Foundry the same flag pauses the run on an approval record with the exact tool input, which expires after five minutes by default and fails closed. Anyone you let in resolves it: in the inspector, over HTTP, or from code:

scripts/approve.tstypescript
const [pending] = await foundry.approvals({ runId: run.id, status: "pending" });
if (pending) await foundry.resolveApproval(pending.id, "approve");   // or "deny"

If someone changes their mind mid-run, run.steer("Hold 2.4.0, include #421 first") interrupts the run cleanly and restarts it in the same conversation with the new instruction.

Arrival: Slack, email, webhooks

Inbound channels are transmissions: typed definitions of how events arrive (verify, normalise, classify) and how replies go out (an outbound transmission becomes a validated tool once installed). An application bundles them so an instance can install "Slack". Playbooks route each event to the right instance and conversation, and a playbook subscription can create the instance on the first message. Delivery is idempotent per event. Credentials stay in adapters you own. The full walkthrough is apps & transmissions; on Kuiper, email arrives as a signed webhook to your unit.

Layers: a fast agent in front, workers behind

The reviewer from step 6 ran inside the release assistant's turn. Work that takes minutes should not: list sibling agents in delegates and the assistant hands it off with glove_foundry_delegate, keeps talking, and hears back in the same conversation. A worker that needs something asks with glove_foundry_ask. Decide the layers by how fast each part must reply, what it must remember and how long it runs; see Layered agents.

Access: the APIs you didn't write

  • Typed client, above: route names are checked against your agent files.
  • OpenAI-compatible /v1/chat/completions and /v1/responses: point any OpenAI SDK or chat UI at an instance; conversations stay durable.
  • Run API /v1/runs with live events, and REST over every record at /api/*.
  • Inspector and WebChat at /: every instance, conversation, run, model call, tool call and approval.

Already built for you

PrimitiveWhat you getInstead of building
Definitionsglove-foundryThe file path is the id. Every field can be a function of the instance, conversation and incoming message.agents/<route>/agent.ts → export default defineAgent({ … })a factory that rebuilds the agent per request from a switch over tenants
Instancesglove-foundryPersisted agents with their own id, context, workspace, installations and playbooks.client.agent(route).create({ context }) · defineAgentInstance(definition, { … })an agents-per-customer table and lookup
Conversationsglove-foundryAny number of durable threads per instance with exact transcripts.client.createConversation(agentId) · client.conversationTranscript(…)thread tables and routing messages to the right history
Installable capabilitiesglove-foundryCapabilities mount only on instances that install them, with typed config.defineSharedTool · defineApp · defineMcp · install(definition, config)per-customer feature flags around tool lists
Shared workglove-foundryWorkspace entries, tasks, a shared inbox and scoped values that agents coordinate through.glove_foundry_workspace · _tasks · _shared_inbox · _environmentcoordination tables between agents
Data adaptersglove-foundryInstances, conversations, activations and workspace data persisted atomically; swap in a database for several hosts.new FileFoundryDataAdapter({ file }) · FoundryDataAdapterserialising runtime state yourself
Schedulesglove-foundryCron, interval and one-shot work, declared in code or created by the agent, rebuilt on restart.defineSchedule({ timing: { kind: "cron", expression }, message }) · glove_foundry_schedulecron jobs and a job queue that call your agent
Sleepglove-foundrySuspend the run and wake the same instance and conversation later.glove_foundry_sleep({ kind: "for", duration: "20m", message })storing 'check back later' state and a timer
Spawn and backgroundglove-foundryStart another instance's run, or continue work without blocking the reply.glove_foundry_spawn · glove_foundry_backgroundfire-and-forget promises that die with the request
Schedule managementglove-foundryThe agent lists and edits its own reminders and recurring work.glove_foundry_schedules({ action: "list" | "update" | "pause" | "resume" | "cancel" })a reminders CRUD API for the agent
Transmissionsglove-foundryTyped inbound events and outbound delivery for one channel; outbound becomes a validated tool when installed. Credentials stay in your adapter.defineTransmission({ inbound, outbound, account })a webhook handler and an API client per channel
Applications and connectionsglove-foundryChannels bundled into something an instance installs; long-lived listeners started only while something needs them.defineApp({ inbound, outbound, connections }) · defineConnection(…)polling loops and socket clients you keep alive yourself
Playbooksglove-foundryRules that match events to instances, with idempotent delivery into the right conversation.composePlaybook({ transmission, match, directives })a dispatcher that decides which agent handles which event
Subscriptionsglove-foundryAn event can create the agent that handles it: zero, one or many instances provisioned atomically.definePlaybookSubscription(…) · runtime.putPlaybookSubscription(…)onboarding code that creates an agent before its first message
Inbound triageglove-classifier/foundryClassify events before any agent starts, and match playbooks on the answers.classifyInbound({ classifier, question }) · classifierPredicate(…)waking an LLM agent to read every event
Approvalsglove-foundryGated tool calls pause the run on an expiring, fail-closed approval record with the exact input.requiresPermission → client.resolveApproval(id, "approve")an approvals table and a pause/resume flow
Steeringglove-foundryInterrupt running work cleanly and restart it in the same conversation with the new instruction.handle.steer(message) · POST /v1/runs/:id/steercancel-and-resend logic that races the old run
Inspector and WebChatglove-foundryEvery instance, conversation, run, model call, tool call and approval, plus a chat to talk to any instance.glove foundry dev → http://localhost:4141an admin dashboard and a debug chat
Event streamglove-foundryCorrelated events by definition, instance, conversation and run, live or after the fact.GET /api/events (SSE) · defineSubscriber(…)log scraping to reconstruct what an agent did
Typed clientglove-foundry/clientCreate instances, send messages, and wait on, stream, cancel or steer run handles, with route types generated from your files.createFoundryClient<FoundryRoutes>({ baseUrl }).agent(route)a hand-written HTTP client for your agent service
OpenAI-compatible APIglove-foundryAny OpenAI SDK or chat UI talks to a durable instance and conversation.POST /v1/chat/completions · POST /v1/responsesan adapter so existing chat clients can reach your agent
Run API and RESTglove-foundryAsynchronous runs with live events, and REST over every runtime record.POST /v1/runs · GET /v1/runs/:id/events · /api/*a job-status API around your agent
Request authorizationglove-foundryOne check in front of the API, the inspector and the event streams. Foundry refuses a public bind without it.defineApplication({ requestAuthorization: { authorize } })auth middleware in front of each route

Next

The release desk works on your machine, with its state in .data/. In step 8 it goes to production on Kuiper: the same folder, no Dockerfile, state on a persistent disk, models through a gateway, and its runs in the dashboard.

The whole path

  1. 01A loop and a toolLoop · Tools
  2. 02Show your workSurface
  3. 03Control what it seesContext
  4. 04Make it rememberState
  5. 05Give it a workspaceWorkspace
  6. 06Split the workTeam
  7. 07Make it a systemIdentity · Time · Arrival · Oversight · Access
  8. 08Put it in productionRun · Keep · Reach · Watch