Write down who your agents are and what each one does, so every app sees the same team. Optional if you have one assistant.
Help me write down my agent team in Mainmind. Ask me which jobs I want
covered, suggest one agent for each job and who it reports to, and show me
the list. When I agree, save it in Mainmind and ask me to approve it.
1Paste the promptPaste the prompt into the AI app connected to Mainmind. It asks which jobs you want covered.
2Check the listCheck the list it shows you, then approve it when asked. Only the space's owner can approve, and a big team may need more than one approval.
3Open TeamOpen Team in Mainmind once it is approved. Every app you connect then sees the same team.
If saving fails, your assistant says why and what is still waiting. You can ask it again.
For your agent
The steps your AI app follows. You don't need to read them.
Serving versus open
Serving:restore_agent_team records a team from plain fields and writes the Role documents for you; export_agent_team hands the same team back as one portable package. A co-owner may raise that restore; conserved Role documents still need an Owner ruling. boot and whoami restore a Worker-validated Role map and colleague directory when one exists. A designated index that declares agent-team: [] is a complete, deliberately empty team. A Markdown seats table in the body is still not parsed — boot now says so and says how many rows it is refusing to guess at. Optional live harness from a recorded start is operational D1, omitted when unknown, and never written into the space's knowledge.
Open: Mainmind does not spawn host bots unattended. A package may carry a per-seat host template so you can rebuild a bot on that host yourself; nothing in Mainmind creates it for you. A host teammate roster is still that host's adapter, not a restore package. An example in the Organizational Seed is a separate cut.
Record a team without writing YAML
restore_agent_team is the write path. Give it seats in plain fields and it composes roles/agent-team.md and every seat Role, frontmatter included, then raises one governed decision bound to the exact documents it composed. Nothing is written until a human rules.
JSON
{
"run_id": "<an open run you own>",
"seats": [
{
"slug": "alder-cos",
"name": "Chief of staff",
"seat": "cos",
"charter": "Runs the weekly loop and routes work",
"process_bounds": ["processes/weekly-review.md"],
"channels": ["ops"]
},
{
"slug": "alder-catalog",
"name": "Catalog",
"seat": "agent",
"reports_to": "alder-cos",
"charter": "Keeps the catalog current",
"channels": ["ops"]
}
]
}
List order is boot order unless every seat states its own boot_order. The Role path comes from the display name — roles/chief-of-staff.md — unless you pass role_path. One call carries at most 18 seats, because the index and every seat Role are proposed as one governed change and a ruling that lands writes its Decision in the same commit.
Recording a seat does not create its profile or bind ownership. For an owner-connected team, register or reuse the intended profile first and use its exact slug in the seat. If that job is already recorded, restore the whole team with the profile's slug in it and in every reports_to that names it, so they are proposed together; a reports_to naming no one in the list is refused. Legacy machine-member setup remains supported separately. A credential never rides in a package, in this tool, or in the conversation.
A Role that already exists keeps its own prose and its own frontmatter keys — its compartment, its write class, anything else somebody chose. Only the seat keys and the host-template block belong to the package. Restoring a team that is already recorded exactly as it stands changes nothing and says so.
ORG.md may list the seat Role paths itself instead of naming an index. The tool refuses that layout rather than guessing which of the two files you meant; point ORG.md at one index path with propose_change first.
Carry a team to another harness
export_agent_team returns one mainmind.agent-team/1 package: every seat's display name, charter, seat, boot order, reports-to, Process bounds, channels, Role path and stored host template, plus an occupancy report. Pass that package's seats to restore_agent_team on the other space — the seats it emits are exactly the seats that tool accepts. A host teammate roster is not this package.
seats_without_member names structural seats with no live identity in the destination. Reuse or deliberately register the intended destination profile, then bind its exact slug through the governed Role writer. Do not assume the source profile's live authority moved between spaces. hosts_without_template names the seats that carry no host recipe. The package is read through the same access gate as any other knowledge: it can only carry seats your connection may already read, and only seats boot would restore.
The host half
A seat may carry the host's own per-seat team template — profile, memory, skills, routines, plugins — in a host_template field. Mainmind stores that template beside the seat and hands it back on export so you can rebuild that one bot; it does not interpret it and does not define a second recipe format. Pass a host's exported team template through unchanged.
That template is not a host teammate roster. The roster of who the host thinks is on the team stays on the host: regenerate it from the seats in the space's knowledge and live members after restore. Do not feed the roster to restore_agent_team.
What Mainmind does check: the envelope is that shape and no other key, the serialized template is at most 24,000 bytes, it cannot break out of its own code fence, and it contains no credential-shaped text — a vendor token, a private key, or a Mainmind machine credential. A template carrying one is refused before it can reach the space's knowledge.
The document is not the authority on what a valid template is.export_agent_team puts whatever it finds stored back through the same gate a submitted template passes, so bytes that reached a seat Role by some other route — a hand-written propose_change, a checkout landing — cannot be served as a template that is malformed or credential-bearing. A refusal is reported by seat and reason in template_refusals rather than silently dropped, and the refused value itself is never echoed.
Be precise about what that check is and is not: it re-checks shape and contents, not provenance. A well-formed block carrying no credential reads the same whoever wrote it. Provenance is what the three binding rules enforce instead: no value the package renders — a seat name, a charter, a Process bound, a channel — may contain the marker or a code fence; a Role whose own prose already carries a marker is refused rather than merged; and a document carrying two markers reads as none, because it is making two claims about one seat. Between them the marker means one thing, the block Mainmind itself wrote, and the re-validation is the backstop for everything those rules cannot see.
A value that cannot sit on one frontmatter line — a line break in a seat name, charter, Process bound or channel — is refused on the way in, rather than written and read back as something else. Quotes and backslashes are fine: writers emit a YAML single-quoted scalar when JSON would have escaped them, and the reader undoes that quoting so the next harness sees the same words. A hand-authored "C:\notes" stays a Windows path; double-quoted scalars are literal.
Where it lives
Default index: roles/agent-team.md. ORG.md may override with agent-team: — a list of Role paths, or one index path.
Boot reads frontmatter, in the same flat dialect the projection already parses. It does not read a body table. restore_agent_team writes that frontmatter; the schema below is what it produces and what boot reads back, not something you have to type.
Index shape
At Alder & Ash the index lists Role paths:
YAML
---
kind: Role
title: Agent team
access-scope: core
write-class: conserved
agent-team:
- roles/chief-of-staff.md
- roles/catalog.md
---
ORG.md may list those same paths instead, or name this index as one path.
A team with deliberately no seats keeps its designated index and declares that state explicitly:
YAML
---
kind: Role
title: Agent team
access-scope: core
write-class: conserved
agent-team: []
---
Put this declaration on the designated index, not on ORG.md. Ready boot and whoami then return recorded: true, seats: [], and declared_empty: true, with no restore gap. Omitting agent-team: from an otherwise empty index leaves the team undeclared and is a restore gap.
Each seat Role
Each listed Role is itself a seat:
YAML
---
kind: Role
title: Chief of staff
access-scope: core
write-class: conserved
seat: cos
boot-order: 1
member: alder-cos
charter: "Runs the weekly loop and routes work"
process-bounds:
- processes/weekly-review.md
channels:
- ops
---
A catalog agent reports to that seat's slug (alder-cos), not the Role path basename:
An agent seat adds reports-to: naming another seat's resolved slug: the Role's member when that field is set, otherwise the path basename. reports-to: chief-of-staff is refused when the chief's slug is alder-cos. Exactly one cos when any seat is present. At most 40 seats. member is optional; the path basename is the slug when it is absent. A slug that is not a live member still sits in the team, empty until a live member holds it: a person joins with invite_member, an agent with register_agent rather than a separate bot login, and the team is recorded with that member's exact slug. charter: is optional and at most 200 characters, the same bound invite_member puts on a live member's charter.
A single Role with seat: and no agent-team: list is itself a one-seat team. Its member: must use the exact profile slug when it binds an owner-connected agent. A display name is not a binding.
Use harness-agnostic names another host can restore: names, Roles, Processes and channels, not vendor channel IDs or host-only UI steps.
What boot returns
Structured team: { recorded, path, cos, seats, declared_empty?, unread?, error? }. declared_empty: true means the designated index explicitly states agent-team: []. Each seat carries slug, seat, boot_order, reports_to, charter, process_bounds, channels, path, name, and member (live row without a credential) or null. Text names the chief of staff and boot order when recorded.
A boot that names one job on the team is a summary, so it fits in what an AI app shows at once. It opens with that job's Role document, its Process bounds and, for a standing agent, its home. AUTHORITY.md follows in full while it is 6 KB or less; a longer one is named instead, with a line telling the agent to ask before any spend, send, publish or other outside write until it has read it. ORG.md and the other always-read documents are named for read_node instead of inlined. team keeps cos and at most three related jobs (the job itself, who it reports to, then the chief of staff and its reports). The job's own row is whole; the others carry only slug, name, kind, boot order, reports-to and path. It adds seats_total. colleagues, the inbox, open asks, open runs and agents you can take over keep their first three beside their counts. summary says where the rest is: whoami returns the whole team and every colleague, page_work pages assignments, list_runs lists runs, and boot with the same job and full: true returns the whole boot as before.
When a space designates a team index, an undeclared or unreadable index is a restore gap. Boot never guesses seats from a Markdown body table. A space with no designated map can still use an owner-connected profile; role_unbound is an honest state, not a reason to invent a seat.
Ready boot and whoami also return that same team object, so an org.read caller can see cos and bound seats without team.read. They return colleagues too: every non-revoked live member (person or machine), with display name, kind, access role, and the bound Role (path, seat, reports-to, portable channels) when one exists. Unbound members still appear, with no invented seat. This is a join of live membership and Role occupancy, recomputed every call — not a Team table. Invite codes, credentials, credential presence, knowledge-scope grants, invitation expiry and Team-management actions stay on Owner list_members. Occupancy is not identity.
Bind an owner-connected profile by putting its exact stable slug in Role member: through the governed writer: restore_agent_team, as in Record a team without writing YAML. Invited people and legacy machine members can also occupy a Role under their existing setup. Unbound live identities still appear. If a start recorded a live harness label, the directory shows it as operational D1 and omits it when unknown; do not write that label into the space's knowledge or treat it as a vendor account id. HTTP GET /api/boot uses the same function as MCP boot. Team management stays Owner list_members.
Owner list_members includes the same team object beside the access roster.
What is not the map
A seats Markdown table in the body is not parsed. Extra keys in frontmatter are ignored. Profiles, credentials, owner grants and contact are not stored in a Role or portable package. A host teammate roster is an adapter to regenerate, never portable durable state and never restore input. propose_change remains the hand-written route for anything restore_agent_team does not cover. Agents working together works with exact live profile slugs whether or not this optional map exists.