Real features, running in production

Eight things Foundations built on Root — with the prompts to build them yourself.

Access tiers, member search, office hours, door access, a provider directory, room booking, event hosting, and the office shopping list. Each example shows how the feature works across the Root boundary and ends with a complete prompt you can hand to your coding agent.

Platform owns conversationOne loop, memory, history, and delivery layer
Tenant owns business truthNo duplicate membership or booking logic
Contracts cross the boundarySigned tools out; authenticated APIs back in
Humans remain reachableEvery channel and tier can escalate
Start with what you want to build

There is nothing better than a worked example.

You do not need all of Foundations. Pick the feature closest to what your community needs, see exactly how it's implemented, then copy the prompt at the end of the chapter into your coding agent and adapt it to your systems.

The implementation walkthrough

Eight features. Eight prompts.

Every chapter shows the member experience first, then the responsibility split between Root and the tenant backend. Expand a chapter for the implementation details and a complete, copyable build prompt grounded in the production code.

01
Access tiers

Set up access: members, employees, mentors

Everything else builds on this. Foundations stayed the only authority on who is a member, a company employee, a visiting mentor, or a stranger — and taught Root to ask for an identity decision instead of copying membership logic into the platform. One person gets the right capabilities on every channel; everyone else gets a smaller, honest surface.

Example request“Can you get me set up with door access?” — sent by a member, an employee, and a stranger
What the member experiences

The same words produce three different results. The member's access is reset, the company employee gets the employee-scoped access tools, and the stranger is told what Foundations is and how membership works — disallowed tools are removed before planning, so the model never sees them.

Slack / email identityPOST /integration/identity/resolveStable principal_ref + tierTier-filtered tools + prompts
Root platformShared kernel
  • Caches the principal against the backend's immutable principal_ref and links Slack/email identities to it.
  • Anchors memory, preferences, follow-ups, and history to the principal, not to an email address.
  • Filters tools and layers tier-scoped prompt sections before the LLM ever plans.
FoundationsTenant backend
  • Resolves Slack ids and emails against members, company_employees, and mentors — in that order, with employees also matched by Slack profile email.
  • Returns tier (member / company_employee / mentor / public), is_admin, and compact profile context; deactivated people resolve as public.
  • Represents email delegates (EAs, historical CCs) as the real sender plus an acts_for mentor, never as impersonation.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

The whole contract is one endpoint plus manifest declarations. POST /integration/identity/resolve receives a channel identity and returns a principal with an immutable ref — the member or employee row id, never an email, because emails and Slack accounts change and a new ref would silently fork the person's memory. The manifest declares the tier vocabulary, gates every tool with a tiers array, and ships tier-scoped prompt_sections so the agent knows why a small surface is small — otherwise it treats member-only capabilities as malfunctions and escalates.

// Root -> Foundations
POST /integration/identity/resolve
{ "channel": "slack", "slack_user_id": "U0123ABC" }

// Foundations -> Root
{
  "principal": {
    "principal_ref": "3c85…immutable-member-uuid",   // employees: "employee:<uuid>"
    "display_name": "Priya Shah",
    "email": "priya@example.com",
    "tier": "member",
    "is_admin": false,
    "profile_context": "Founder at Example Co; Seattle member."
  }
}
Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
You are adding identity + access tiers to my community backend so it can act
as the integration behind a Root agent platform tenant (protocol root-int/1).
Root owns all conversations and calls my backend to decide who a person is.
My database stays the only authority on membership — never store membership
state on the platform side.

Context you can assume:
- I have tables for members (id uuid, name, email, slack_id, status), company
  employees (id, name, email, slack_id, status, company_id), and external
  mentors/office-hours hosts (id, name, email). Adapt to the actual schema.
- Root sends signed HTTP requests: x-root-signature is an HMAC-SHA256 of
  "<timestamp>.<rawBody>" using our shared signing secret, x-root-timestamp
  carries the timestamp. Verify both and reject stale timestamps (>5 min).

Build the following:

1. POST /integration/identity/resolve
   Body: { "channel": "slack"|"email", "slack_user_id"?, "email"? }.
   Resolution order for Slack: members.slack_id → company_employees.slack_id
   → company_employees by the Slack profile email. For email: members.email
   → company_employees.email → mentors.email (only when that mentor has a
   current or upcoming hosted session), else tier "public".
   Respond: { "principal": { principal_ref, display_name, email, tier,
   is_admin, profile_context, traits } }.
   - principal_ref MUST be my immutable row id ("<uuid>" for members,
     "employee:<uuid>" for employees, the mentor uuid for mentors) — never an
     email or name. Root anchors long-term memory to this ref; returning a
     different ref for the same human silently creates a second memory.
   - Tier vocabulary: member, company_employee, mentor, public. Deactivated
     members resolve as public, not as their old tier.
   - profile_context: 1–2 sentences (< 300 chars) the agent can use, e.g.
     "Founder at Example Co; Seattle member since 2024."

2. Email delegation (executive assistants, calendar delegates, past CCs):
   when the sender is not the mentor but thread/CC history shows they act
   for one, return the sender as the principal (tier public) PLUS an
   "acts_for" block: { reason: "current_cc"|"historical_cc"|…, principal:
   <the mentor's principal> }. Root's authorization gate uses this to allow
   actions on the mentor's own things while refusing anything else. Never
   resolve the assistant AS the mentor.

3. GET /integration/roster — all active principals (ref, tier, email,
   slack_user_id, display_name, is_admin), INCLUDING employees and mentors.
   Root sweeps this to heal stale cached tiers; anyone missing from the
   roster gets demoted, so forgetting employees here locks them out.

4. Manifest declarations: "access_tiers": ["member","company_employee",
   "mentor","public"]; every tool gated with "tiers": [...]; and tier-scoped
   "prompt_sections" that tell the agent WHO each tier is and what to do
   instead of escalating (public: membership is application-based, office
   hours and directories are member benefits — explain, don't apologize;
   mentor: visiting host with no Slack and no member portal, building access
   arrives automatically as a day-of door code).

5. Tests: the same message from a member, an employee, a mentor, and a
   stranger produces four different tool surfaces; a deactivated member
   resolves public; an employee whose Slack profile email differs from the
   roster email still resolves by slack_id.

What to copy

  • Let your system remain authoritative for identity; return a small, durable contract.
  • Immutable refs. Tiers are your vocabulary. Admin status is orthogonal to tier.
  • Give every limited tier a prompt section — gating without explanation reads as breakage.
Source mapFoundations: src/integration/rootPlatformAdapter.ts · resolveIdentity / rosterRoot: src/channels/identity.tsRoot: src/platform/principals.tsRoot: src/agent/emailAuthorizationGate.ts
03
Office hours

Run a full office-hours program in chat

Mentors host blocks of 30-minute slots; members discover, book, reschedule, and cancel in plain language; calendar invites, guests, and door codes handle themselves. This is Foundations' most-used feature and the best template for any booking workflow.

Example request“Does Bill have open office hours next week? Book me the noon slot and bring Matt.”
What the member experiences

Root lists Bill's open slots, books the noon one with the member's topic, resolves “Matt” to a member and adds him to the invite, and the Google Calendar event goes out to everyone — including a day-long front-door code if any guest isn't a member.

list_office_hoursbook_office_hours (slot id)Guest resolution by idCalendar event by iCalUIDDoor code if needed
Root platformShared kernel
  • Handles the conversation: disambiguating the mentor, confirming the slot, collecting the topic.
  • Runs availability + booking as separate tool calls so the model never invents a slot id.
  • Lets hosts manage their own blocks over email with the same tools, gated by the mentor tier.
FoundationsTenant backend
  • Owns office_hours blocks and time_slots; booking is a transactional slot claim with a one-slot-per-session guard.
  • Creates one Google Calendar event per booking keyed by iCalUID `-booking` so retries update instead of duplicating.
  • Issues day-long UniFi door codes to non-member hosts and non-member guests, embedded in the invite.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

Two tables carry the whole program: office_hours (mentor, date, window, status) and time_slots (30-minute rows with status, booker, topic). The listing tool returns real slot ids; the booking tool accepts only those ids, so the LLM can't hallucinate a time. Calendar work is idempotent by construction — every booking event is keyed by iCalUID <slot_id>-booking, so re-running a booking updates the same event, and a previously-cancelled event is explicitly revived to confirmed (Google keeps cancelled tombstones that will otherwise eat your invites). One hard-won rule: the server runs in UTC but business dates are Pacific — from 5pm to midnight, CURRENT_DATE is already tomorrow, which once made every evening availability query return zero slots.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Build a complete office-hours program on my backend, exposed to my Root
tenant as integration tools (root-int/1 — follow my existing signed
/integration/tools/* + manifest patterns).

1. Schema:
   - office_hours: id, mentor_id, date, start_time, end_time, status
     (active/cancelled), calendar_id (the block event's iCalUID).
   - time_slots: id, office_hour_id, start_time, end_time, status
     (available/booked), booked_by_<channel-id>, booked_by_name, topic,
     duration_minutes.
   Slots are generated when a block is scheduled (default 30 min).

2. Member tools (tiers ["member"]):
   - list_office_hours { mentor_id?, from_date?, to_date? } → blocks with
     their OPEN slots, each carrying its real slot id. The booking tool must
     only accept those ids — never let the model invent a time.
   - book_office_hours { time_slot_id, topic?, additional_guests?: string[] }
     → transactional claim (UPDATE … WHERE status='available'), with a
     one-slot-per-session guard per member. Resolve guests through the
     shared person resolver (names / handles / emails); surface unresolved
     guests in the reply, never drop them silently.
   - view_my_bookings, cancel_booking { time_slot_id },
     reschedule_booking { time_slot_id, new_slot_id },
     update_booking_topic { time_slot_id, topic } — topic updates must NOT
     rebuild the attendee list; re-read the calendar event's current
     attendees so later-added guests survive.
   - search_past_office_hours { mentor_name?, attendee_scope? } for "who did
     I meet with" questions; a "hosted" scope lets a mentor see their own
     past sessions' attendees.

3. Host tools on the email surface (tier ["mentor"]):
   schedule_hosted_office_hours, list_hosted_office_hours,
   get_office_hours_registrants, reschedule_hosted_office_hours,
   cancel_hosted_office_hours. A reschedule must carry every existing
   attendee to the new event automatically.

4. Calendar integration (Google or equivalent), idempotent by construction:
   - One event per booking keyed by iCalUID "<slot_id>-booking"; the block
     itself keyed by the office_hours row's calendar_id. Upsert by iCalUID:
     search first, patch if found, insert otherwise.
   - CRITICAL: if the existing event's status is "cancelled" (a tombstone
     from an earlier cancellation), explicitly set status "confirmed" when
     rebooking — patching a cancelled event leaves it cancelled and no
     invites go out.
   - Store absolute instants; pass the business timezone as display only.

5. Door access hooks (adapt to my access system):
   - A non-member HOST gets a door code valid the entire day of their
     session, created when the block is scheduled, revoked on cancel,
     re-issued on reschedule; embed it in their invite and confirmation.
   - Non-member GUESTS on a booking share one day-long code, embedded in
     the calendar invite. Members never need one.
   All hooks best-effort: an access-system outage must never block booking.

6. Timezone rule (this bug shipped twice): the DB and Node run in UTC but
   business dates are local. Never use CURRENT_DATE / NOW()::date or
   new Date().toISOString().split('T')[0] for "today" — from 5pm local to
   midnight they're already tomorrow. Bind "today" computed in the business
   timezone as a query parameter everywhere.

7. Ship the workflow coaching as manifest prompt_sections — the tools alone
   don't teach the agent the conversational rules:
   - tiers ["member"], channels ["slack","api"]: "book a meeting" means an
     office-hours booking; list first, then book; distinguish BOOKING from
     HOSTING (someone offering times wants to host, not grab a slot); if a
     mentor is fully booked, share their contact info instead of promising
     to watch for openings.
   - tiers ["member","mentor"], channels ["email"]: members emailing about
     office hours are usually HOSTING; email manages the resolved mentor's
     own blocks only — never attendee bookings, and never scheduling a
     block for a third party (refuse plainly; a delegate's authority does
     not transfer hosting).

8. Tests: book → cancel → rebook sends a fresh invite (tombstone revived);
   evening availability shows the right day; a guest name that matches two
   members asks for clarification instead of guessing.

What to copy

  • List returns ids; mutate accepts only ids. That single rule removes a whole class of hallucination.
  • Idempotent calendar writes keyed by a deterministic UID — and revive tombstones explicitly.
  • Make every side-effect hook (calendar, door codes, notifications) best-effort so the booking itself never fails on a dependency.
  • Put workflow rules that span tools (booking vs hosting, sequencing, per-channel semantics) in manifest prompt_sections, scoped by tier and channel — they version with your backend, not with the platform.
Source mapFoundations: src/agent/tools/office-hours.tsFoundations: src/slack/services/booking.tsFoundations: src/services/googleCalendarService.ts · createOrUpdateEventFoundations: src/services/officeHoursVisitorPassService.ts
04
Space access

Grant building access and let guests in — from chat

Foundations runs two offices on two different door systems: UniFi in Seattle, ButterflyMX in San Francisco. Root grants and resets access, buzzes guests in, and mints temporary door codes — with the sensitive paths deliberately kept off email, where a spoofable From header must never open a door.

Example request“I'm in SF on Tuesday — can you set me up?” · “Someone's at the front door, can you let them in?”
What the member experiences

Root creates the SF tenant record through a browser automation and DMs the member their personal registration link (the vendor's own emails are unreliable). For the guest, Root remotely releases the Seattle front door and logs who asked — or mints a door PIN valid until end of day and DMs it.

Member asks in SlackTier + channel gateDoorbot API / dashboard automationCode or unlockDM'd, never posted publicly
Root platformShared kernel
  • Enforces the tool surface per tier and channel — door tools exist on member Slack and API, never on email.
  • Keeps the audit trail: every unlock and pass is attributed to the requesting principal in the conversation log.
  • Delivers codes privately: a request from a public channel gets its answer by DM with a scrubbed public reply.
FoundationsTenant backend
  • Talks to Seattle UniFi through a small self-hosted doorbot HTTP API (create/activate/deactivate accounts, visitor passes, remote unlock).
  • Drives the SF ButterflyMX property dashboard with a headless-browser automation — including scraping each new tenant's registration link because vendor emails get spam-filtered.
  • Ties access to lifecycle: granted when members/employees are added, revoked automatically on deactivation, re-keyed on email change.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

Seattle is clean: a small doorbot service on the office network exposes an authenticated HTTP API over the UniFi Access system, and Foundations calls it for account activation, momentary remote unlock, and time-boxed visitor PINs. San Francisco has no usable API, so Foundations drives the ButterflyMX property dashboard with a headless browser — create tenant, re-key email, revoke, mint visitor pass — and scrapes the tenant's personal registration link off the dashboard so Root can deliver it directly instead of hoping the vendor's email arrives. The security posture is in the surface, not the prompt: door tools are simply absent from the email tool surface, and a code requested in a public Slack channel is DM'd with the public reply scrubbed.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Add building-access capabilities to my Root integration backend (root-int/1).
My access systems: [DESCRIBE YOURS — e.g. UniFi Access reachable through an
internal HTTP service, and/or a vendor with no API where a browser automation
against their admin dashboard is acceptable]. Follow my existing signed
/integration/tools/* + manifest patterns.

1. Tools and gating (the security model lives in the SURFACE):
   - request_building_access {} — grant/reset the CURRENT user's access for
     an office. Tiers ["member","company_employee"]; channels
     ["slack","api"] plus email ONLY for non-sensitive resets.
   - check_building_access {} — read-only status across offices; all tiers
     that can hold access.
   - unlock_front_door {} — momentary remote release. Channels
     ["slack","api"] ONLY — NEVER email: a spoofable From header must not
     produce instant physical entry. Attribute every unlock to the
     requesting principal in the result payload.
   - create_guest_door_pass { office, valid_until? } — temporary door PIN,
     max 24h, defaulting to end of the local day. Same channel restriction.
     When invoked from a public channel, DM the code to the requester and
     return a scrubbed result so the public reply just points at the DM.
   - request_access_for_teammate { person } — a member grants a teammate
     (same company) access; resolve the person through the shared resolver
     and track the grant so it's revoked when the teammate is removed.

2. Vendor adapters:
   - API-based system: a thin client for the internal service (activate /
     deactivate / visitor pass / remote unlock), authenticated with an API
     key. Visitor-pass PINs are only returned at creation — persist them
     immediately; they expire on their own, so no cleanup job.
   - Dashboard-only system: a headless-browser automation (persist the
     session across runs). After creating/resending a tenant, scrape their
     personal registration link from the tenant detail page and RETURN it —
     vendor registration emails are unreliable, so the agent delivers the
     link in its reply. Never post that link in a public channel.

3. Lifecycle integration (the part everyone forgets):
   - Grant access automatically when a member/employee is created or a
     teammate grant lands; revoke automatically on deactivation.
   - Email changes re-key door-access accounts in BOTH systems (they're
     keyed by email); a missing account under the old email is a graceful
     no-op, a refusal falls back to a human task.
   - Track every grant/revoke as a retryable task: attempt, retry once on
     failure, then escalate to a human intervention. A deactivated person
     silently keeping door access is the failure mode to design against.

4. All hooks best-effort and fire-and-forget from the caller's perspective:
   an access-system outage never blocks the request that triggered it.

5. Tests: unlock is absent from the email tool surface; a guest pass from a
   public channel never echoes the PIN publicly; deactivating a person
   queues revocations in every system they had access to.

What to copy

  • Gate by construction: sensitive tools are absent from risky surfaces, not "discouraged" in the prompt.
  • Return vendor registration links directly when vendor email is unreliable — don't leave people waiting.
  • Access is a lifecycle, not a request: grant on add, revoke on remove, re-key on email change, escalate on failure.
Source mapFoundations: src/agent/tools/unifi.ts · butterflymx.ts · door-unlock.tsFoundations: src/services/butterflyMxService.tsFoundations: src/services/bookingGuestVisitorPassService.tsFoundations: src/services/accessChangeMonitorService.ts
05
Service providers

A trusted provider directory members build themselves

“Know a good startup lawyer?” gets answered from the community's own experience. Members recommend providers with ratings and reviews in conversation; Root searches the directory the same way it searches members — and the whole dataset stays self-serve.

Example request“Know a good startup lawyer?” · “Add a rec: we loved Acme Legal, 5 stars, super fast on our seed round.”
What the member experiences

Root returns providers ranked by community rating with review snippets and who recommended them. The new recommendation is saved attributed to the member, who can edit their rating, update the review, or remove it later — all in chat.

Free-text askHybrid search + rerankRanked providers + reviewsSelf-serve add / edit / remove
Root platformShared kernel
  • Handles the conversational side of contributing: extracting provider, category, rating, and review from a natural message.
  • Keeps contributions attributed to the resolved principal, so “my recommendations” needs no extra auth.
  • Surfaces the directory to every member channel — Slack, email, and the member API — from one tool set.
FoundationsTenant backend
  • Owns providers and recommendations (member attribution, 1–5 rating, review text, category, contact info).
  • Regenerates a provider's neutral AI summary description fire-and-forget whenever reviews change.
  • Publishes provider pages into Root's knowledge base so general questions can ground on them too.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

Two tables: providers, and per-member recommendations with the rating and review. Search reuses the exact hybrid-retrieval pattern from member search — embeddings + keyword + fast rerank. Members manage only their own recommendations (matched on the resolved principal, not on anything the model claims), and a provider's displayed description is an AI-written neutral summary regenerated in the background whenever its reviews change, so no single review becomes the pitch.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Build a community service-provider directory on my Root integration backend
(root-int/1, following my existing tool + manifest patterns).

1. Schema:
   - service_providers: id, name, category, website, contact info,
     ai_description, embedding, tsvector.
   - provider_recommendations: id, provider_id, member_id, rating (1-5),
     review text, created_at/updated_at. One recommendation per member per
     provider (upsert on conflict).

2. Tools (tiers ["member"], channels ["slack","email","api"]):
   - search_service_providers { query, category? } — hybrid retrieval
     (vector + keyword) then a fast-model rerank. Return structured results:
     [{ provider_id, name, category, avg_rating, recommendation_count,
     top_review_snippets, recommended_by }].
   - recommend_service_provider { provider_name, category?, rating, review,
     website?, contact? } — match to an existing provider first (fuzzy, via
     LLM over candidates) so "Acme Legal" and "Acme Legal LLP" don't fork;
     create the provider if genuinely new. Save the recommendation
     attributed to the RESOLVED principal — never trust a name in the
     message for attribution.
   - list_my_service_providers {} — the member's own recommendations.
   - update_my_provider_review { provider_id, rating?, review? } and
     remove_service_provider_recommendation { provider_id } — own rows only,
     matched on principal ref.
   - update_service_provider { provider_id, ...fields } — factual fixes
     (website, contact) open to any member; keep an audit trail.

3. Derived description: when recommendations change, regenerate the
   provider's ai_description fire-and-forget with a writing-quality model —
   a neutral 2-3 sentence summary of what the community actually said.
   Never block the recommendation write on it.

4. Knowledge sync: publish each provider as a knowledge page through the
   platform's kb sync API (stable external_id = provider id) so general
   questions ("how do I find a bookkeeper?") can ground on the directory
   even without the search tool.

5. Tests: a recommendation for a near-duplicate name attaches to the
   existing provider; a member can edit only their own review; the rerank
   surfaces a well-reviewed provider above a keyword-only match.

What to copy

  • Attribution comes from the resolved principal, never from message text.
  • Fuzzy-match provider names on write — directories die by near-duplicate rows.
  • Reuse one hybrid-search pattern across every directory you expose.
Source mapFoundations: src/agent/tools/service-providers.tsFoundations: src/services/embeddingService.tsFoundations: src/services/platformKnowledgeClient.ts
06
Conference rooms

Book the conference room without double-booking it

Huddle 1 is one physical room serving two systems: ad-hoc conference bookings and the office-hours program. Foundations made Root the front door for both, with availability checked across both tables, auto-confirmation, calendar invites, and an in-room tablet that always shows the truth.

Example request“Can I book Huddle 1 tomorrow 2–3? Invite Maya.”
What the member experiences

Root checks the slot against both conference bookings and office-hours sessions, books it, resolves Maya by id, and sends the invite. If Maya weren't a member she'd get a door code for the day inside the invite. An EA emailing on a member's behalf can manage the same booking — bounded by the authorization gate.

Availability across both systemsAuto-confirmed bookingGuests by id + door codesICS + calendar eventTablet display
Root platformShared kernel
  • Runs the conversational flow on every channel, including the delegate email path with per-action authorization.
  • Keeps bookings attributed to the acting member even when an assistant manages them.
  • Lets members change or cancel by replying — the tools take booking ids, the platform handles the dialogue.
FoundationsTenant backend
  • Owns conference_room_bookings and enforces no-overlap against office-hours slots for the same room.
  • Sends the invite (ICS + Google event, idempotent by UID) through a queue worker; chat and web edits share it.
  • Feeds the in-room tablet from the same tables, so the display can never disagree with the bookings.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

The room's availability check is a union across two tables — conference bookings and booked office-hours slots — because one physical room backs both programs. Bookings auto-confirm (no approval step), the calendar invite is assembled by a queue worker keyed by a deterministic UID, and guests go through the same shared resolver + non-member door-code service used everywhere else. The delegate case matters in practice: an EA emailing for a member gets the booking tools only when the resolved mentor is an active member, the booking attaches to the member's record, and an authorization gate re-checks every action against who the delegate acts for.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Build conference-room booking on my Root integration backend (root-int/1,
following my existing tool + manifest patterns). Rooms: [LIST YOURS — e.g.
"Huddle 1"]. Some rooms may be shared with other scheduling systems (in
Foundations, the office-hours program) — availability must consider all of
them.

1. Schema: conference_room_bookings — id, room, member_id, date, start_time,
   end_time, purpose, calendar_guests (structured JSONB: resolved guests
   with name/email/member_id), status (confirmed/cancelled), calendar
   event UID, guest door-pass columns.

2. Availability = a union query across EVERY system that can occupy the
   room (bookings + any other program's booked slots for the same room).
   Reject overlaps at write time inside a transaction — the conversational
   check-then-book gap is real.

3. Tools (tiers ["member"], all channels; see #5 for delegates):
   - submit_conference_room_booking { date, start_time, end_time, purpose?,
     calendar_guests?: string[] } — auto-confirmed; resolve guests through
     the shared person resolver; store them structured, not as raw strings.
   - list_my_conference_room_bookings {}
   - update_conference_room_booking { booking_id, ...changes } — guests are
     replaceable/removable after confirmation; refresh the invite for
     everyone on any change.
   - cancel_conference_room_booking { booking_id }

4. Calendar delivery through a queue worker (chat AND web edits share it):
   ICS attachment + provider event, idempotent by a deterministic UID per
   booking; update on change, cancel on cancel. If any resolved guest is
   NOT an active member, mint one shared day-long door code for the
   booking, embed it in the invite, revoke on cancel, re-issue on
   reschedule (date changed). Best-effort: invite/door failures never
   block the booking.

5. Delegate support (assistants booking for a member over email): when the
   email surface resolves an acts_for member, include these tools, attach
   the booking to the MEMBER's record, and run every call through the
   authorization gate so the delegate can only manage that member's
   bookings. Non-members with no acts_for member get nothing.

6. If a physical display shows the room's schedule, feed it from the same
   tables — never a copy.

7. Tests: an overlap with the other program's slot is rejected; a non-member
   guest's invite contains a door code; an EA can reschedule their
   member's booking but cannot book for anyone else.

What to copy

  • Availability is a property of the physical room, not of one table.
  • Route chat and web mutations through one queue worker so invites can't fork.
  • Delegation attaches actions to the represented member, with authorization on every call.
Source mapFoundations: src/agent/tools/conference-rooms.tsFoundations: src/models/conferenceRoomBooking.tsFoundations: src/queue · conference-room-calendar-queueFoundations: src/agent/emailAuthorizationGate.ts (delegate path)
07
Event booking

Host events end to end: request, approve, RSVP, walk in

Members propose events in chat; the team approves; Root handles everything downstream — RSVPs in plain language, emailed calendar invites, per-event door codes, and self-serve cancellation that actually cleans up after itself.

Example request“I want to host a founder dinner at the space next month.” · later: “Count me in for the dinner!”
What the member experiences

The request lands in the team's approval queue with everything they need. Once approved, members RSVP conversationally, anyone can ask Root to email them a calendar invite that works in any client, a door code scoped to the event window is created automatically, and the host can see their RSVP count — or cancel the event with attendees notified and the door pass revoked.

submit_event_requestTeam approvesRSVPs + .ics invitesEvent-window door passHost cancels → full cleanup
Root platformShared kernel
  • Carries the whole lifecycle conversationally: proposing, editing, RSVPing, checking counts, cancelling.
  • Parses “count me in” / “I can't make it” into structured RSVP calls against the right event.
  • Escalates ambiguity (“which dinner?”) instead of guessing which event a reply refers to.
FoundationsTenant backend
  • Owns event requests, calendar events, RSVPs, and public-event sign-up captures.
  • Issues a per-office visitor door pass for each approved event, valid from just before start to just after end.
  • Implements approved-event cancellation as a real workflow: calendar removal, attendee invite cancellations, pass revocation, team notification.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

Event hosting is a request/approval workflow, not a direct write — members submit and edit requests in chat, admins approve, and approval triggers the downstream machinery: the calendar event, a visitor door pass scoped to the event window at the right office, and RSVP tracking (members-only events use RSVP rows; public events capture sign-up emails). The detail that saves the most support email: send_calendar_invite emails a standards-compliant .ics for any event — the fallback for calendar apps the web button doesn't cover — and it accepts the event's id, name, or a pasted event-page URL, resolving all three. Cancelling an approved event is itself a tool, because "remove from calendar + cancel attendee invites + revoke the door pass + tell the team" should never require an admin.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Build event hosting on my Root integration backend (root-int/1, following my
existing tool + manifest patterns): member-proposed events with team
approval, conversational RSVPs, emailed calendar invites, and door access
scoped to the event.

1. Schema:
   - event_requests: id, member_id, title, description, proposed date/time,
     expected_attendance, status (pending/approved/denied/cancelled).
   - calendar_events: the approved event (public or members-only), with a
     public_token for shareable pages.
   - RSVPs: members-only events get rsvp rows (accepted/declined/tentative);
     public events capture { email, name } sign-ups. Both are also the
     door-access eligibility list.

2. Member tools (tiers ["member"]):
   - submit_event_request { title, description, date, start_time, end_time,
     expected_attendance? } — lands in the team's approval queue with an
     admin notification.
   - list_my_event_requests / update_event_request (pending only) /
     cancel_event_request (pending only).
   - cancel_approved_event_request { event_id } — the important one. A host
     cancelling an APPROVED event must trigger full cleanup: remove the
     calendar event, queue attendee invite cancellations (members-only),
     revoke the event's visitor door pass, notify the team. No admin in
     the loop.
   - rsvp_to_event { event_ref, response } — parse "count me in" / "can't
     make it" conversationally; write the RSVP or public sign-up capture.
   - check_event_rsvps { event_id } — counts for the host; attendee emails
     visible only to the host and admins.
   - send_calendar_invite { event_ref, email? } — email a
     standards-compliant .ics for the event. Accept the event id, its name,
     OR a pasted event-page URL (resolve the public_token). Send only to
     the member's own email unless an admin directs otherwise.

3. Approval hook (admin side, not a member tool): approving creates the
   calendar event AND fires a visitor door pass for the event's office,
   valid ~30 min before start to ~1 hr after end. Recognized attendees
   (RSVP/sign-up/member) can retrieve entry instructions; the pass is
   revoked on deny/cancel and reconciled when the event moves. Pass
   creation is fire-and-forget — it never blocks approval.

4. Strip door codes from every general-purpose calendar tool result; codes
   are only delivered through the gated access flows.

5. Tests: RSVP by event name disambiguates when two events match; a pasted
   /event/<token> URL resolves for send_calendar_invite; cancelling an
   approved event revokes its pass and queues attendee cancellations.

What to copy

  • Approval is the trigger for all downstream automation — one hook, everything consistent.
  • Accept ids, names, and pasted URLs anywhere members reference an event; they will paste URLs.
  • Self-serve cancellation must clean up everything the approval created.
Source mapFoundations: src/agent/tools/event-requests.ts · calendar.tsFoundations: src/services/eventVisitorPassService.tsFoundations: src/services/eventAccessService.ts
08
Costco list

Run the office shopping list — down to the real order

The silliest-sounding feature is the best systems lesson: members say “add sparkling water to the Seattle list,” Root matches it to a real product from purchase history, and an admin submits the actual Costco order — through hard safety gates, on a designated card, from an automation host on the office network.

Example request“Add sparkling water to the Seattle Costco list.”
What the member experiences

Root matches the request against the catalog of everything the office has ordered before (falling back to a live storefront search), drops the real product into Seattle's basket as a recommendation, and the member can list or retract it later. Only an admin can turn the basket into a real order — and the checkout refuses to run without the saved office address, the designated card, and an explicit kill-switch flip.

Free-text requestCatalog LLM matchLive search fallbackOffice basketAdmin-only checkout with hard gates
Root platformShared kernel
  • Keeps the member/admin split honest: submit-order tools are admin-only and excluded from autonomous API surfaces entirely.
  • Makes the basket watchable: members can set action alerts on recommendations like any other community-visible tool.
  • Handles the long latencies gracefully — a live storefront search can take a minute; the tool contract carries its own timeout.
FoundationsTenant backend
  • Maintains its own product catalog scraped from the account's order history; matching favors what the office actually buys.
  • Runs the browser automation on a Mac mini on the office network — the retailer blocks datacenter IPs and cloud browsers, so residential hardware is the integration point.
  • Enforces checkout safety host-side: saved office delivery address matched, designated card last-4 matched, and an env kill-switch before the final click.
How it's built — and the full prompt to build yoursExpand

How Foundations implements it

Matching is deliberately two-stage: a fast LLM pass over catalog candidates first (cheap, biased toward what the office has bought before), then a live storefront search on the automation host only when the catalog misses, and an unmatched request is saved for admin resolution rather than dropped. The automation itself couldn't live in the cloud — the retailer's identity provider blocks datacenter IPs and CDP-fingerprinted browsers — so a Playwright server runs on the office Mac mini and the backend talks to it as a typed HTTP client. Checkout is gated three ways on the host: the office's saved delivery address must be selected, a saved card matching the designated last-4 must be found, and a kill-switch env var must be explicitly true or the whole flow reports as a dry run.

Build it yourselfA complete prompt for your coding agent, based on the Foundations implementation
Build an office shopping-list pipeline on my Root integration backend
(root-int/1, following my existing tool + manifest patterns). Retailer:
[YOURS — Foundations uses Costco Same-Day, which has no API and blocks
cloud browsers, hence the design below].

1. Catalog: a products table of everything the account has ordered
   (scraped from order history on a schedule) plus items discovered via
   storefront search. Dedupe on the retailer's product id when derivable,
   else case-insensitive name; track times_ordered for match ranking.

2. Member tools (tiers ["member"], channels ["slack","email","api"]):
   - recommend_costco_item { request, office } — resolve free text ("more
     sparkling water") to a real product: (a) fast-model LLM match over
     ILIKE-prefiltered catalog candidates FIRST — cheap and favors what we
     actually buy; (b) live storefront search on the automation host as
     fallback (30-60s — set a generous per-tool timeout_ms in the
     manifest); (c) if still unmatched, SAVE the raw request for admin
     resolution — never silently drop it. Item lands in the office's
     basket as "recommended", attributed to the member.
   - list_costco_basket { office } and
     remove_costco_recommendation { item_id } — own items only.

3. Admin-only tools (never on member or autonomous-API surfaces):
   - submit_costco_order_admin { office, dry_run? } — order the APPROVED
     basket items; record an orders row with an item snapshot, total, and
     confirmation/error; stamp ordered items.
   - sync_costco_catalog_admin {} — refresh the catalog from order history.

4. The automation host: if the retailer blocks datacenter IPs / cloud
   browsers, run a small Playwright HTTP server on a machine on a
   residential/office network. Endpoints (API-key authed): session status,
   order-history sync, product search, submit order. One browser operation
   at a time (in-process queue); session cookies snapshotted to disk after
   every operation. My backend gets a thin typed HTTP client with long
   client-side timeouts — search minutes, order tens of minutes.

5. Checkout hard gates, enforced HOST-side (defense against everything
   upstream): (a) the office's saved delivery address must be selected at
   checkout (street-substring match); (b) a saved card ending in the
   designated last-4 (limited-credit office card) must be found and
   selected — no card, no order; (c) an ALLOW_CHECKOUT env kill-switch:
   anything but "true" makes every submit stop before the final click and
   report as a dry run. Rebuild the cart from the basket on every submit
   so the order exactly matches what was approved. Dump screenshot + HTML
   on failure.

6. Tests: "more of the usual coffee" matches the catalog without touching
   the browser host; an unmatched request appears in the admin queue; a
   submit with the kill-switch off completes as a dry run and never
   places an order.

What to copy

  • Match against your own purchase history before any live lookup — cheaper, faster, and it's what people mean.
  • Unmatched ≠ dropped. Save it for a human.
  • Real-money actions get layered hard gates enforced at the last hop, plus a kill-switch that defaults to safe.
Source mapFoundations: src/agent/tools/costco.tsFoundations: src/services/costcoService.ts · costcoBasketService.tsFoundations: scripts/costco-automation/ (reference host)
A sensible order for your implementation

Start thin. Add depth where your community is unique.

Foundations became broad over years. A new tenant can get the architectural benefits with one identity endpoint and one valuable tool, then grow the manifest without changing the platform.

1
Connect one channelUse web test first, then Slack or email.
2
Return one stable principalProve identity and tier behavior before mutations.
3
Expose one read toolGenerate it from a real service and verify the manifest.
4
Add one idempotent actionCarry the invocation id through the write boundary.
5
Sync the knowledge you ownUse stable external ids and an outbox.
6
Wire human interventionsMake every incomplete edge case land somewhere trackable.
7
Flip ownership explicitlyOne event, one worker, one source of truth.