Rebuilds / Grok Bot

Rebuild Grok Bot

Grok Bot gives you bots you keep around: AI teammates with names, jobs and context that builds up over time. They work in the apps you connect, like Gmail, on a cloud computer they share, pass work to each other, and come back when something needs your approval. This page builds the same shape in Foundry and deploys it on Kuiper. Two parts are worth copying: there is one bot definition and every bot is data, and every app a person connects is a Foundry app installed with their account.

Grok Bot launched in beta on 11 August 2026 and runs on Cursor's cloud. SpaceXAI picks the model, with no picker and automatic failover. Here the operator sets one model for every bot (BOT_MODEL), with no failover.

›

An independent example built from the Grok Bot docs on cursor.com and docs.x.ai (sources at the end). It is not affiliated with or endorsed by xAI (SpaceXAI) or Cursor, and it is not how Grok Bot is built. The code is in examples/foundry-bot-crew, and pnpm verify runs all of it offline, against local servers that use Google's and Linear's MCP tool names.

What Grok Bot does

  • Bots with a name, a job and a working style. Your first bot is made for you and starred. A new one asks what you want it around for, or you give it a name, a job and a working style. Each keeps its memory and preferences across sessions; files live on the shared computer. You can have up to 50.
  • Bots are siblings, one starred. There is no coordinator kind. One sibling is starred as the Primary Bot: it takes the asks that name no bot and hands specialists their part. The star is data you can move. You can also write to the bot whose job it is, and a bot can hand part of its work to another. Each bot's conversation is its own.
  • Apps belong to your account. Your bots work in the apps you connect: Gmail, your calendar, your tracker. A bot asks for access when it needs an app, and once you connect it, every bot can use it.
  • One computer for all your bots. A persistent cloud computer with a browser, filesystem and terminal. Your bots share its files, browser sessions and logins, each on its own screen. Isolation is per person: strict between people, but your bots are not a boundary from each other.
  • Sign-ins stay with you. For passwords, 2FA and CAPTCHAs you take over the bot's screen; other secrets go through a masked request, never chat.
  • Bots work together. They message each other and pass ownership of a task. A group chat holds two to six bots: they decide who answers, you can hand one of them a request, and @everyone reaches all of them.
  • Skills and routines. A skill is reusable instructions any bot can use, invoked with /. You save one from chat or by showing a bot the workflow in the browser (“Teach a task”). A routine assigns a workflow to one bot and runs it on a schedule or after an event, up to 50 per bot.
  • Copies and templates. Duplicating a bot copies its profile, skills and routines, not its memory or history. A template shares a bot's instructions, chosen memories, skills, routines and first-party integrations, without chats, keys, custom MCP servers or scripts, and a marketplace lists ready-made bots.
  • Approvals. Sending, buying, deleting and publishing wait for you: allow once, deny, or make an always-allow rule. Require-approval rules win, and sensitive actions can pass through Auto Review first. A direct message from you takes priority over background work and can redirect the current turn.
  • Team Bots (public beta, 28 September 2026). An owner publishes one shared bot. Each teammate gets a private chat the owner can't read, memory is team-wide plus per person, and a custom Slack app can be deployed.

How work moves

posts@mention@mentionwrites totellsfix itfixedwritesconnect_appGmail sendnew mailnew mailOffsite crewgroup chat · no hostMayathe appSales Outboundwas “New bot”Bug Triageroutine: new mailEngineerfixes on a branchInbox Managerstarred · new mailWorkspace: mayaaccounts · skills · one computerApprovalMaya says yesHer appsGmail · Lineartake_job
1 / 10
  1. Maya makes a bot: one createAgent call with no name or job. It asks what she wants it around for. She says overnight outbound, so it calls take_job, which writes a name and a description to its own instance. “New bot” is now Sales Outbound, with her connected apps. Nothing was deployed.
  2. Bug Triage needs Linear, so it asks with connect_app. Maya signs in to Linear once; her account is saved in your store and the Linear app is installed with it on every one of her bots.
  3. The bots are siblings: one definition, different data, and no coordinator kind. Maya writes to Bug Triage in its own chat, and that message is what starts it. An ask that names no bot would go to her starred bot, Inbox Manager, which hands specialists their part.
  4. Bug Triage files the issue in Linear (Maya always allows that), finds Engineer with glove_mesh_list_agents and hands it the fix with glove_mesh_send_message. That handoff is what starts Engineer.
  5. Engineer's final answer goes back to the conversation that sent it the work, as a new turn. Nobody checks in.
  6. In its own chat, in a sentence or two: filed as ENG-1, fixed on a branch, with a test.
  7. Each member keeps its own conversation for the Offsite crew chat, and the app shows them merged. Maya's message goes to the bots it @-mentions, so Sales Outbound and Bug Triage answer and Inbox Manager, not mentioned, stays quiet. @everyone, or no mention at all, reaches every member.
  8. Maya describes her Monday signup sweep. save_skill waits for her review, then stores it in her workspace, and any bot runs it with /monday-signups. The routine belongs to Sales Outbound: a schedule it sets with glove_foundry_schedule. A routine after an event is data on the bot, written by add_routine.
  9. Sales Outbound finds the signups with gmail.search_threads in code, then calls the Gmail send. Foundry holds the run with the exact email until Maya approves, then delivers it from her address in its own process. request_sign_in works the same way: approving it is Maya signing in, and the bot never sees the password.
  10. The Gmail app's connection watches Maya's inbox and passes each new message in. Inbox Manager and Bug Triage each have a routine on new mail, so both wake, each in its own chat: Bug Triage passes (not a bug), and Inbox Manager replies in Lena's thread once Maya approves.

One definition, every bot in data

The tempting build is a folder per bot: agents/sales-outbound/, agents/inbox-manager/. Then you can't make a bot, because each new one is a code change, and renaming one is a deploy. It is like Twitter keeping a page in its codebase for every user. Instead, agents/bot/agent.ts says what any bot can be, and each bot is an instance of it:

Grok BotIn this buildChanged by
A bot's name, job and working styleThe instance's name, description and context.styleThe bot itself with take_job, or your app with configureAgent
Your bots, up to 50The instances in your workspaceMaking (makeBot) or deleting a bot
Your Primary Botcontext.starred on one instanceYour app with starBot
Connected appsAn account in your store, and on every bot of the person an installation with that account (plus Gmail's routes and a send binding)Your Connect button: the provider's sign-in, then connectApp
“Always allow” and require-approval rulesThe installation's config.alwaysAllow and config.alwaysAsk; asking winsYour app, when the person sets them
A bot's chat with youA conversation on that bot, key chatYour app, when the bot is made
A group chat and who is in itA conversation per member, each with context.group.members; no hostYour app (openGroup)
SkillsWorkspace entries, skill:<name>A bot with save_skill, after you review it
Routines on a scheduleSchedules on the bot that owns them, plus any a copy or template brought in context.schedulesA bot with glove_foundry_schedule
Routines after an eventThe bot's context.routines, which builds its playbookA bot with add_routine
The shared computerOne working environment per workspaceThe bots, as they work

None of these needs a deploy. See Agents are data for the general rule.

Siblings, and what starts a bot

The bots are siblings: the same definition, different data, and no coordinator kind. One sibling is starred as the Primary Bot. It takes the asks that name no bot and hands specialists their part. The star is context.starred, data you can move with starBot. What makes a bot run is the request that reaches it:

What starts a botIn FoundryWhere it lands
The person writes to itsend(botId, …)That bot's chat
The person asks without naming a bot (the starred one)askPrimary sends it to the starred botThat bot's chat
It is @-mentioned in a group chat, or nobody is (this build's rule)postToGroup sends to the bots a message names, or to all membersIts side of the group
A sibling hands it workglove_mesh_send_message to its instance idA thread on it; the answer goes back on its own
Its routine on a scheduleA schedule it set with glove_foundry_scheduleIts own conversation
Its routine after an event, like new mailA playbook built from its context.routinesIts chat

One event can wake more than one bot. New mail reaches every bot whose routine asks for it: Inbox Manager answers it, Bug Triage files it if it reports a bug.

This harness uses 19 primitives across 12 parts. Everything listed is already built; the dashed box is what you write.

1. What a bot can be

The definition lists everything a bot could have: the apps a person may connect, memory, the shared computer, the REPL, the mesh, the new-mail playbook. Which of it a given bot gets is decided from its data on each run. The file only assembles; the prompt and tools live beside it.

agents/bot/agent.tstypescript
export default defineAgent({
  description: "A bot: an AI teammate with its own job and memory, working in its owner's apps and on a computer it shares with their other bots",
  components: composeAgent(gmailApp, linearApp, memory),   // apps a bot may have, each installed with its owner's account

  model: botModel,                                   // from the bot's data
  systemPrompt: (_agent, ctx) => botPrompt(ctx),     // from the bot's data
  store: ({ conversationId }) => FileStore.open(conversationId),
  memory: [memory],

  tools: (_agent, ctx) => botTools(ctx),             // take_job, connect_app, save_skill, add_routine, request_sign_in, skills
  skills: (_agent, ctx) => slashSkills(ctx.data, ctx.workspaceId),  // the library, invoked with /name
  workingEnvironment: computer,                      // one per workspace
  repl: () => defineRepl({                           // the apps' reads, as functions
    language: "javascript",
    session: JsSession.create(),
    mount: { discovery: "auto" },
    programmaticTools: { select: ({ apps }) => apps.flatMap((app) => app.tools.filter((tool) => tool.readOnly)), keepDirect: false },
  }),
  mesh: { waitMs: 60_000 },                          // siblings message each other by id

  // A routine after an event: new mail reaches the bots whose data asks for it.
  playbooks: (_agent, { agentInstance, installations }) => {
    const mail = installations.find((item) => item.kind === "application" && item.id === gmailApp.id);
    const routines = (agentInstance.context.routines ?? []).filter((item) => item.on === "new-mail");
    return mail?.accountId && routines.length > 0 ? [newMailFor(mail.accountId, routines)] : [];
  },
  // Routines on a schedule that came as data, with a copy or a template.
  schedules: (_agent, { agentInstance }) => (agentInstance.context.schedules ?? []).map((item) => defineSchedule(item)),

  accountSessions,                                   // the credential boundary (lib/accounts.ts)
});

The prompt reads the instance. A bot with no job is told to ask for one; any other bot is told its own job and working style, that its siblings are equals it can hand parts to, its routines, its owner's apps and how to send email from their address. The starred bot is also told that asks naming no bot come to it: it does its part and hands each sibling theirs with glove_mesh_send_message.

2. A person's bots are records

A person's workspace is their team. Their bots are plain data in lib/people.ts. The example seeds Maya, her sample Gmail account and her first bot, Inbox Manager, starred, whose routine is on new mail; your app creates everyone else's when they sign up, with makeBot (at most 50 per person), and stars their first bot:

foundry.application.ts (excerpt)typescript
const mayaGmail = accountFor(MAYA, "gmail", sampleConnection(MAYA, "gmail").identity, true);
if (!(await readAccount(mayaGmail.id))) await saveAccount(mayaGmail, sampleConnection(MAYA, "gmail").tokens);

const mayaInbox = defineAgentInstance(bot, botFor(MAYA, {
  id: "maya-inbox",
  name: "Inbox Manager",
  job: "Keeps Maya's inbox moving: answers what it can from her address and hands the rest to the bot whose job it is.",
  style: "Short replies in Maya's voice. Checks with her before promising a date or a price.",
  starred: true,                             // her Primary Bot: asks that name no bot come here
  routines: [{ on: "new-mail", instruction: "Read each new email. Answer the ones you can; hand the rest to a sibling." }],
  installations: [installationFor(mayaGmail)],
}));

export default defineApplication({
  name: "Bot crew",
  data,                                      // seeds written only when the id is missing
  services,                                  // your accounts, plus routes and bindings in a file
  routes: [gmailRoutes(mayaGmail.id).in, gmailRoutes(mayaGmail.id).out],
  bindings: [sendBinding(mayaInbox.id, mayaGmail.id)],
});

3. A bot names itself

A new bot asks what it is for. When Maya answers, it calls take_job, a tool that writes its own instance:

agents/bot/tools.ts (excerpt)typescript
{
  name: "take_job",
  description: "Take on the job your owner described: give yourself a short name, one sentence saying what you do, and your working style if they gave one.",
  inputSchema: z.object({
    name: z.string().min(1).max(40),
    description: z.string().min(1).max(300),
    style: z.string().max(200).optional(),
  }),
  async do({ style, ...job }) {
    const current = await Effect.runPromise(ctx.data.getAgent(ctx.agentId));
    const context = style ? { ...current!.context, style } : current!.context;
    await Effect.runPromise(ctx.data.putAgent({ ...current!, ...job, context, updatedAt: new Date().toISOString() }));
    return { status: "success", data: { renamed: job.name } };
  },
}

The bot's next run reads the new name, job and working style, and the next time a sibling lists the team it sees “Sales Outbound”. Your app can rename a bot the same way with configureAgent.

4. Apps a person connects

An app is a Foundry app, installed per bot with the person's account, and it holds everything about its provider. Gmail reads and drafts through Google's Gmail MCP server, sends through its transmission, and hears about new mail through a connection:

in codebearersenddeliverswatchnew mailA botMaya'sgmail app · mcpreads, draftsgmail app · outboundsend, after approvalgmail app · connectiongmail-watchGmail MCP serverGoogleGmail APImessages.sendGmail pushPub/SubaccountSessionsyour tokenssearch_threads
1 / 4
  1. The app's MCP server is opened with the installation's account. Its reads are functions in the bot's REPL, grouped under the app, so one program can search, read and filter, and only the answer comes back into context.
  2. Google's server has no send, and a send should not run inside the agent anyway. The transmission's send tool waits for Maya to approve the exact email; Foundry then delivers it in its own process, for a bot whose binding grants it.
  3. Foundry runs the connection once per connected account. It renews users.watch, pulls Gmail's notifications, reads what arrived with history.list and passes each message in; a playbook decides which bot hears it.
  4. All three parts ask accountSessions for an operation on Maya's account: mcp:connect, gmail:send, gmail:watch. Her tokens stay in your store and are refreshed there. Foundry never holds them.
agents/bot/apps/gmail.app.tstypescript
export default defineApp({
  description: "Gmail: search, read and draft mail, send with approval, and hear about new mail",
  config: z.object({
    alwaysAllow: z.array(z.string()).default([]),  // "Always allow"
    alwaysAsk: z.array(z.string()).default([]),    // require-approval rules, which win
  }),
  inbound: [gmail],                // new mail, as events
  outbound: [gmail],               // sending: Google's server has no send tool
  connections: [gmailWatch],       // users.watch, Pub/Sub, history.list
  mcp: {
    entry: mcpServer("gmail"),     // https://gmailmcp.googleapis.com/mcp/v1, or a local server for a sample account
    auth: mcpCredentials(),        // the person's token as a bearer header, inside accountSessions
    tools: askUnlessAllowed(["search_threads", "get_thread", "get_message", "list_drafts", "list_labels", "create_draft"]),
  },
});
  • Reads come from the provider's MCP server, opened with the installation's account. The tool policy runs reads and drafts, and asks before anything else unless the person chose “Always allow” for it. A tool in alwaysAsk asks even if it is always allowed.
  • Sending is the Gmail transmission's outbound side. Foundry delivers it in its own process, inside the account session, after Maya approves the exact email, and only for a bot whose binding grants gmail:send. The bot never holds the token.
  • Linear is an app that is only an MCP server (https://mcp.linear.app/mcp). Most apps look like that; add a transmission when the provider pushes events or a send must run outside the agent.

Accounts are yours. When a bot needs an app it asks with connect_app; your app shows that approval as a Connect button that runs the provider's sign-in. The callback saves the account and tokens in your store, then writes what Foundry needs:

lib/connect.ts (excerpt)typescript
export async function connectApp(foundry, person, app, { tokens, identity, alwaysAllow }) {
  const account = accountFor(person, app, identity);
  await saveAccount(account, tokens);                          // your store; Foundry reads it, never writes it
  if (app === "gmail") {
    await foundry.putRoute(gmailRoutes(account.id).in);        // new mail
    await foundry.putRoute(gmailRoutes(account.id).out);       // sending
  }
  for (const bot of await foundry.agents()) {                  // apps are the person's: every bot gets it
    if (bot.workspaceId !== person.id) continue;
    await foundry.install(bot.id, installationFor(account, alwaysAllow));
    if (app === "gmail") await foundry.putBinding(sendBinding(bot.id, account.id));
  }
  return account;
}

Foundry reads accounts through your account directory (services in foundry.application.ts), keeps routes and bindings in fileTopologyStore, and asks accountSessions for each operation (mcp:connect, gmail:send, gmail:watch), which refreshes tokens as they expire. Grok Bot keeps secrets out of chat the same way, with a masked request.

5. Bots read their apps in code

The REPL turns each installed app's reads into a group of functions. Sales Outbound finds this week's signups in one program instead of a chain of tool calls, and only the result comes back into context:

what Sales Outbound runsjavascript
execute_js({ code: `
  const found = gmail.search_threads({ query: "subject:signup" });
  found.threads.map((thread) => thread.latest.subject)
` })
// → ["New signup: Dana Lee (Northwind)", "New signup: Ravi Patel (Globex)"]

Writes stay direct tools, so Maya can see them: the send waits for her, and filing a Linear issue runs because she always allows it. A person with ten connected apps does not put ten catalogues in context: keepDirect: false keeps the reads in code.

6. Bots pass work over the mesh

mesh gives every bot the Glove Mesh tools, hosted by Foundry. Its peers are the other bots in its workspace, so a bot only ever sees its owner's team. When part of a task is a sibling's job, a bot lists them, picks the one whose job fits, and sends it that part by id. Maya tells Bug Triage about a crash; it files the bug in Linear and passes the fix to Engineer. The final answer comes back to the conversation that sent the work, as a new turn.

what Bug Triage receivestext
<mesh-message id="msg_…" thread="thread_…" from="maya-bot-3" name="Engineer" status="replied">
Patched on branch fix/empty-export: export returns an empty file for an empty report. Added a test.
</mesh-message>

An ask that names no bot goes to the starred bot. askPrimary sends it to that bot's chat, and its prompt says it is starred, so it does its part and hands each sibling theirs. When Maya asks how the week looks, Inbox Manager answers for the inbox and asks Sales Outbound for the leads:

lib/groups.ts (excerpt)typescript
export async function askPrimary(foundry, personId, text) {
  const primary = primaryOf(await foundry.agents(), personId);   // the bot with context.starred
  return foundry.send(primary.id, chatWith(primary.id).id, text);
}

A group chat has no host. Each member keeps its own conversation for it, whose data lists the members, and your app shows them merged. In Grok Bot the bots decide who answers. In this build, postToGroup sends the person's message to the bots it @-mentions, or to every member for @everyone or no mention. A member with nothing to add replies with nothing, which posts nothing. A bot brings a sibling in the same way it hands off work, and the answer comes back to its side of the group.

lib/groups.ts (excerpt)typescript
export async function postToGroup(foundry, chat, text) {
  const team = await foundry.agents();
  const members = chat.members.map((id) => team.find((agent) => agent.id === id) ?? { id });
  for (const botId of addressed(text, members)) {               // the mentioned bots, or all of them
    await foundry.send(botId, groupConversationId(botId, chat), text);
  }
}

7. One computer, one skill library

The computer is a working environment saved per workspace with Foundry's own persistence, so files one bot writes, another can read:

agents/bot/computer.tstypescript
export const computer = defineWorkingEnvironment({
  options: { stdlib: [fetchFiles({ maxResponseBytes: 2_000_000 })] },
  persistence: foundryDataEnvironmentPersistence({ scope: "workspace" }),
});

Saves replace the whole snapshot, so if two bots write at once the last one wins. For bots that work side by side, use a Kuiper sandbox and browser per person: one live machine. As in Grok Bot, isolation is per person, so bots are not a boundary from each other, and deleting a bot leaves its files.

Skills live in the workspace too, as skill:<name> entries. save_skill has requiresPermission, so Maya reads the draft before any bot can use it. The definition's skills turns each entry into a Glove skill, so /monday-signups in a message to any bot puts that skill's steps just before the message. Bots find and read skills themselves with glove_read_skill.

A routine belongs to one bot: on a schedule it is a schedule the bot sets with glove_foundry_schedule; after an event, add_routine writes it to the bot's own data and its playbook is built from it on the next run. A bot can have several routines on one event, and add_routine refuses a 51st, counting the bot's schedules.

8. Approvals and sign-ins

Drafting is free; sending waits. The Gmail send tool comes from the transmission's requiresPermission, so Foundry records the exact email, holds the run, and resumes it when Maya answers in the inspector, through the API or in your app. “Always allow” for an app's tool is a write to the installation's alwaysAllow; a require-approval rule in alwaysAsk wins over it. A sign-in waits the same way: request_sign_in hands Maya the screen for a password, a 2FA code, a CAPTCHA or single sign-on. This example has no browser, so the bot says it couldn't sign in; mount a Kuiper browser and hand it over with browser.handOff, as Meta Muse does, for the real thing. A message from Maya while a bot works can redirect it with steering.

your apptypescript
const [pending] = await foundry.approvals({ status: "pending" });
// { toolName: "glove_app_gmail__gmail_send", agentId: "maya-bot-1",
//   input: { routeId: "maya-gmail-out", payload: { to: "dana@example.com", subject: "Quick intro", … } }, … }
await foundry.resolveApproval(pending.id, "approve");

9. New mail reaches the bots whose routine asks

The gmail-watch connection runs once per connected account, and only while a bot has Gmail installed and its playbook wants mail. It calls users.watch daily (Google requires it at least every 7 days), pulls the Pub/Sub subscription, reads history.list with Maya's token, and passes each message in, claimed once by its Gmail id. Each bot with a routine on new mail has a playbook built from it, and the mail lands in that bot's chat:

agents/bot/playbook.tstypescript
export function newMailFor(accountId: string, routines: ReadonlyArray<EventRoutine>) {
  const routes = gmailRoutes(accountId);
  return composePlaybook({
    name: "new-mail",
    transmission: gmail,
    match: { event: mailReceived, routes: [routes.in] },
    directives: routines.map((routine) => ({     // one per routine on new mail
      action: readMail,
      instruction: `${routine.instruction} To answer, reply from your owner's address …`,
    })),
    applications: [gmailApp],
    outbound: [{ route: routes.out, application: gmailApp, instruction: "Reply in the same Gmail thread." }],
  });
}

10. Copies and templates

Both are data, in lib/templates.ts. duplicateBot makes a new bot for the same person, named “<name> (copy)”, with the original's job, working style, routines and app installations. Its schedules come along as context.schedules, which the definition's schedules turns into Foundry schedules on the copy's first run. Its memory and chats stay with the original, and skills are already the person's.

templateOf makes a template: the bot's instructions, the memories its owner picks, skills, routines and first-party apps with their approval rules. It carries no chats, accounts, keys, custom MCP servers or scripts. fromTemplate makes a bot from one with the new person's own accounts, and MARKETPLACE lists two ready-made ones, Inbox Manager and Sales Outbound.

11. Run it

terminalbash
pnpm --filter glove-foundry-bot-crew verify

1. Inbox Manager (maya-inbox), starred, has gmail (maya-gmail) and a routine on new mail; the Gmail watch is connected
2. "New bot" became "Sales Outbound": Renamed to Sales Outbound. I'll start on tonight's batch.
3. Bug Triage asked for Linear; once Maya signed in, all 4 of her bots have it
4. Bug Triage, after asking Engineer: Fixed: Patched on branch fix/empty-export: …
5. after approval: Sent the intro to dana@example.com.
6. a sign-in waits for Maya: You approved the Salesforce sign-in, but I have no browser here to sign in with, so I left Salesforce alone.
7. skill monday-signups saved for the team; Sales Outbound runs it every Monday at 09:00; Bug Triage has a routine on new mail
8. Offsite crew: Sales Outbound and Bug Triage answered; Inbox Manager wasn't mentioned and stayed quiet
9. new mail woke Inbox Manager and Bug Triage. Bug Triage: not a bug. Inbox Manager: Replied to Lena Park about pricing.
10. an ask that named no bot went to Inbox Manager, the starred bot: Inbox is clear. Sales Outbound says: 14 new leads this week, …
11. /monday-signups, saved by Sales Outbound, in Inbox Manager's chat: Running monday-signups from the steps you saved.
12. Sales Outbound duplicated with its job, style, apps and Monday routine, and none of its memory or chats; its template carries no accounts

With no API key every bot runs a short script instead of a model, chosen from its data, and Maya's accounts are samples on local MCP servers, so this checks the wiring, not the judgment. Add ANTHROPIC_API_KEY and run pnpm dev to try it with real models, and pnpm connect <person> gmail to connect a real account.

Deploy on Kuiper

terminalbash
kuiper login
kuiper projects create crew && kuiper use crew
kuiper units create crew

kuiper providers add anthropic --key -            # once per organisation
kuiper secrets set GOOGLE_CLIENT_SECRET=- GOOGLE_SERVICE_ACCOUNT_JSON=- LINEAR_CLIENT_SECRET=- FOUNDRY_TOKEN=-
kuiper env set -u crew CREW_DATA_DIR=/data \
  'FOUNDRY_TOKEN=${{secrets.FOUNDRY_TOKEN}}' \
  'ANTHROPIC_API_KEY=${{models.API_KEY}}' \
  'ANTHROPIC_BASE_URL=${{models.ANTHROPIC_BASE_URL}}' \
  GOOGLE_CLIENT_ID=… 'GOOGLE_CLIENT_SECRET=${{secrets.GOOGLE_CLIENT_SECRET}}' \
  GMAIL_PUBSUB_TOPIC=… GMAIL_PUBSUB_SUBSCRIPTION=… \
  'GOOGLE_SERVICE_ACCOUNT_JSON=${{secrets.GOOGLE_SERVICE_ACCOUNT_JSON}}' \
  LINEAR_CLIENT_ID=… 'LINEAR_CLIENT_SECRET=${{secrets.LINEAR_CLIENT_SECRET}}'

kuiper deploy --source . -u crew --wait
kuiper dashboard -u crew --enable                 # inspector login
  • Models go through Kuiper's model gateway. The provider key stays with the organisation, and each call is counted with tokens and cost.
  • State (bots, threads, accounts and tokens, routes and bindings, transcripts, memory, skills, the computers, schedules) sits on the /data disk and survives redeploys. A new bot or a new connection is a write there, not a redeploy.
  • New mail needs nothing public. The Gmail watch pulls its subscription from inside the unit.
  • The API and inspector are locked by the requestAuthorization adapter in foundry.application.ts, made from FOUNDRY_TOKEN. Set it before the first deploy: Foundry won't listen on a public address without it.
  • A real computer. Mount a Kuiper sandbox and browser per person with glove-execution/kuiper in place of agents/bot/computer.ts, so sign-ins carry over between bots.

Where it differs

  • Grok Bot has plugins invoked with @, native Google Docs, Sheets and Slides, API-key connectors, custom MCP servers, Stripe Link, X (posting is approval-gated) and Plaid (read-only). Here there are two apps, Gmail and Linear; any app with a hosted MCP server is another file like linear.app.ts, and an aggregator's MCP server can stand in for many at once.
  • SpaceXAI picks Grok Bot's model, with automatic failover. Here the operator sets one model (BOT_MODEL), with no failover.
  • Teach a task (a bot watching you work in the browser for up to 10 minutes, then drafting a skill) isn't built here. In Foundry it would start from a Kuiper browser you drive through browser.handOff, with the session recording on; drafting a skill from that recording is the missing part. Here a skill comes from a workflow you describe, through save_skill.
  • There are no Team Bots: one shared bot an owner publishes, a private chat per teammate that the owner can't read, team-wide plus per-person memory, and a custom Slack app.
  • Approvals are per tool, with “Always allow” and “Always ask” kept per installation. In Grok Bot, sensitive actions can also pass through Auto Review first; the Dots rebuild shows an auto-review with its rules kept as data.
  • The 50-routine cap is checked in add_routine and, through configure, in Foundry's glove_foundry_schedule. Both count the bot's event routines, the schedules it carried as data and its own recurring schedules; a one-off reminder isn't a routine.
  • As in Grok Bot, isolation is per person, so bots are not a boundary from each other: they share one computer and every app their person connects. Deleting a bot leaves its files.
  • The computer is a Glove working environment (files and scripts) until you swap in a Kuiper sandbox and browser.

Deliberately left out

Grok Bot can route bot traffic through your desktop to get past datacenter-IP blocks. This build leaves that out on purpose: it exposes your local network to every bot, and it exists to get around sites' blocks.

Sources