Every team has an API: what it owns, who's accountable for what, how to ask it for things, how much it can take on. Usually this information is scattered across wikis, onboarding docs, and people's heads, where it drifts out of date.
Team API as Code solves this by writing it all down: one teamapi.yml per team declaring services, roles, members, interactions, and cognitiveLoad β reviewed in pull requests and versioned in git.
TeamAPI makes that spec executable. It renders diagrams, serves a read-only REST API and an MCP server for LLM assistants, gives any team a chat persona, and generates config for tools like CrewAI and Backstage.
The format is a superset of TeamTopologies/TeamAPI-As-Code, adding roles, people, and cognitive load. The concept comes from Team Topologies; the bounded-context and context-map vocabulary from Domain-Driven Design.
π§ Contents
- π Quick start
- π Examples
- π§ AI-native team knowledge
- π Diagrams
- π REST API
- π₯οΈ Dashboard
- π€ MCP tools
- π¬ Chat
- βοΈ Generators
- π₯ Import from GitHub
- π Sync with GitHub teams
- π» CLI reference
- π°οΈ Org history
- π CI integration
- π Paperclip
- π¬ Slack
- π PagerDuty
- πͺͺ Okta
- π©Ί Checking an integration
- π€ Contributing
π Quick start
Install the CLI from npm:
npm install -g @jgalego/teamapi
Or clone and build from source:
git clone https://github.com/JGalego/TeamAPI.git && cd TeamAPI pnpm install pnpm build
Try it against the sample org bundled with this repo, examples/acme-org:
teamapi validate examples/acme-org
teamapi render examples/acme-org --scope topology
teamapi serve-api examples/acme-org --port 3000
teamapi serve-mcp examples/acme-org # point Claude Desktop/Code at this commandExamples covers ACME Org and the other sample orgs, each modeled on a real company's topology.
π Examples
Every example in this README runs against ACME Org (examples/acme-org), a small fictional e-commerce company: Platform Payments runs the payments-api and ledger services everyone else depends on, Stream Checkout owns the cart and checkout flow, Stream Onboarding handles sign-up and KYC, and Enabling DevEx coaches the other three on testing and delivery practices.
Four more fictional-but-recognizable orgs ship alongside it, each modeled after a real-world team topology:
| Example | Modeled after | Shape |
|---|---|---|
examples/reelstream-org |
Netflix-style streaming platform | Full-cycle stream team (Recommendations) + a delivery platform team + a chaos-engineering enabling team |
examples/meridian-pay-org |
Stripe-style payments infrastructure | A billing stream team, a ledger/payments platform team, and a complicated-subsystem fraud-scoring team it can't safely absorb |
examples/cartwell-org |
Amazon-style marketplace | Two-pizza, single-threaded-owner teams (Search, Fulfillment) plus a seller-enablement team |
examples/wavelength-org |
Spotify-style squads/chapters | A playlists squad, an audio-platform team, and a cross-squad chapter-coaching team |
They work with every command in this README β swap in the path, e.g. teamapi render examples/meridian-pay-org --scope topology.
π§ AI-native team knowledge
A team includes the AI agents working alongside its people, and the knowledge they all draw on. Both live as optional sections in the same teamapi.yml document as everything else:
| Section | What it is |
|---|---|
agents[] |
AI assistants treated as first-class team participants β provider, model, role, capabilities, permissions. |
memory[] |
Persistent organizational memory: architecture decisions, conventions, lessons learned, recurring issues. |
specifications[] |
Specification-driven-development artifacts β requirements/design/tasks/acceptance criteria, with a lifecycle, reviewers, approvals, and linked PRs/issues. |
steeringDocuments[] |
Coding standards, API conventions, security guidelines, architecture principles β inherited organization β team β project by walking the existing platform team-reference chain. |
prompts[] |
A version-controlled, renderable prompt library ({{variable}} templating, with history). |
playbooks[] |
Ordered operational procedures β incident response, release, onboarding β with required roles and automation hooks. |
policies[] |
Machine-readable governance (PR requirements, required approvals, security/dependency policy) for external automation to enforce. |
knowledgeBase[] |
ADRs, FAQs, meeting notes, runbooks, design docs. |
workflows[] |
Process state machines (e.g. testing β approval β deployment β announcement), independent of any particular CI/CD system. |
sessions[] |
A record of AI collaboration sessions: objective, prompts used, artifacts produced, decisions made. |
Every section is optional, so documents written before they existed keep validating unchanged β there's no migration. And like the rest of the toolchain, they're read-only: edited in git, never POSTed.
Worked example β agents mirror team boundaries, the same way services do. In examples/acme-org, platform-payments runs a five-agent fleet (architecture-reviewer, test-generator, security-scanner, docs-writer, compliance-auditor), each scoped narrowly enough that three can review the same OAuth pull request in parallel without contradicting each other; memory/conways-law-for-agents records why that split replaced a single do-everything agent. stream-onboarding, the only team touching raw KYC data, carries a policies/no-agents-on-applicant-pii entry and no agents[] at all β so GET /teams/stream-onboarding/agents returns [] for a documented reason, not an oversight.
Context bundles: POST /context (or the get_context_bundle MCP tool) takes a goal β { "goal": "Implement OAuth" }, optionally scoped to one teamId β and returns just the entries relevant to it from across those sections, plus the scoped team's related teams, members, and services. Ranking is keyword overlap, and each hit carries the matchedTerms behind it rather than an opaque similarity score. It's the one call that orients an assistant on a task without walking the whole graph.
The knowledge graph (GET /knowledge-graph, GET /knowledge-graph/:nodeId/traverse, or the get_knowledge_graph/traverse_knowledge_graph MCP tools) links every team, person, agent, and document by ownership, role, team topology, and resolved cross-team $ref edges, for visualization or traversal tooling to consume.
Each section gets the same read-only REST shape β GET /<plural>, GET /teams/:id/<plural>, GET /teams/:id/<plural>/:resourceId, e.g. /teams/platform-payments/prompts/code-review β plus a matching list_*/get_* MCP tool pair, and all of them are covered by GET /search?q=. POST /teams/:id/prompts/:promptId/render (or render_prompt) fills a prompt's {{variable}} placeholders. Field-by-field reference: docs/spec/teamapi-extended-v1.md.
π Diagrams
teamapi render <patterns> --scope <scope> renders the resolved org graph as Mermaid or DOT, where <scope> is topology, context-map, hierarchy (needs --team <id>), or org-hierarchy. Add --format dot for Graphviz, or --out <file> to write to disk instead of stdout. The diagrams below are ACME Org's.
π Team-interaction organigram: --scope topology
Who talks to whom, and how tightly.
flowchart LR
enabling_devex["Enabling DevEx"]
platform_payments["Platform Payments"]
stream_checkout["Stream Checkout"]
stream_onboarding["Stream Onboarding"]
stream_checkout -->|"platform / x-as-a-service"| platform_payments
stream_checkout -->|"collaboration / depends (Slowing)"| stream_onboarding
stream_onboarding -.->|"facilitating"| enabling_devex
stream_onboarding -.->|"depends (OK)"| platform_payments
classDef default fill:#ede9fe,stroke:#7c3aed,stroke-width:1px,color:#1e1b4b;
πΊοΈ DDD context map: --scope context-map
The same relationships, reinterpreted as DDD patterns β how the underlying software should actually fit together. An explicit contextMappingPattern wins where a team declares one; otherwise it's inferred from the Team Topologies interaction mode (x-as-a-service β OpenHostService, collaboration β Partnership). facilitating is left unclassified: it's coaching, not a runtime integration.
flowchart LR
enabling_devex["Enabling DevEx"]
platform_payments["Platform Payments"]
stream_checkout["Stream Checkout"]
stream_onboarding["Stream Onboarding"]
stream_checkout -->|"CustomerSupplier"| platform_payments
stream_checkout -->|"Partnership (inferred)"| stream_onboarding
stream_onboarding -->|"unclassified"| enabling_devex
classDef default fill:#ede9fe,stroke:#7c3aed,stroke-width:1px,color:#1e1b4b;
π§βπΌ Role hierarchy: --scope hierarchy --team stream-checkout
Who reports to whom on one team, and who's actually sitting in each seat: roles[]/reportsTo annotated with the members[] filling them, laid out top-down like a conventional org chart.
flowchart TD
backend_engineer["Checkout Backend Engineer (Engineer) β Yuki Tanaka"]
frontend_engineer["Checkout Frontend Engineer (Engineer) β Fatima Al-Sayed"]
tech_lead["Checkout Tech Lead (TechLead) β Diego Alves"]
tech_lead --- backend_engineer
tech_lead --- frontend_engineer
classDef default fill:#ede9fe,stroke:#7c3aed,stroke-width:1px,color:#1e1b4b;
π’ Org-wide role hierarchy: --scope org-hierarchy
The same reporting lines, zoomed out to the whole company, one box per team. A solid arrow is formal reporting (reportsTo/reportsToRef, same-team or cross-team); a dashed one is alignsWith, for matrix relationships like a community-of-practice lead a role coordinates with but doesn't report to.
flowchart TD
subgraph enabling_devex["Enabling DevEx"]
enabling_devex__coach["DevEx Coach (DeliveryLead) β Marta Kowalski"]
end
subgraph platform_payments["Platform Payments"]
platform_payments__head_of_engineering["Head of Engineering (EngineeringManager) β vacant"]
platform_payments__ledger_engineer["Ledger Engineer (Engineer) β Lin Zhao"]
platform_payments__payments_engineer["Payments API Engineer (Engineer) β Sam Okafor"]
platform_payments__tech_lead["Payments Tech Lead (TechLead) β Priya Raman"]
end
subgraph stream_checkout["Stream Checkout"]
stream_checkout__backend_engineer["Checkout Backend Engineer (Engineer) β Yuki Tanaka"]
stream_checkout__frontend_engineer["Checkout Frontend Engineer (Engineer) β Fatima Al-Sayed"]
stream_checkout__tech_lead["Checkout Tech Lead (TechLead) β Diego Alves"]
end
subgraph stream_onboarding["Stream Onboarding"]
stream_onboarding__engineer["Onboarding Engineer (Engineer) β Aisha Bello"]
stream_onboarding__tech_lead["Onboarding Tech Lead (TechLead) β Noah Fischer"]
end
platform_payments__head_of_engineering --> platform_payments__tech_lead
platform_payments__tech_lead --> platform_payments__payments_engineer
platform_payments__tech_lead --> platform_payments__ledger_engineer
stream_checkout__tech_lead --> stream_checkout__backend_engineer
stream_checkout__tech_lead --> stream_checkout__frontend_engineer
stream_onboarding__tech_lead --> stream_onboarding__engineer
platform_payments__head_of_engineering --> stream_checkout__tech_lead
stream_checkout__tech_lead -.->|"aligns with"| enabling_devex__coach
platform_payments__head_of_engineering --> stream_onboarding__tech_lead
stream_onboarding__tech_lead -.->|"aligns with"| enabling_devex__coach
classDef default fill:#ede9fe,stroke:#7c3aed,stroke-width:1px,color:#1e1b4b;
π REST API
teamapi serve-api examples/acme-org --port 3000 spins up a live REST API over ACME Org. Open /docs for a Swagger UI with a "Try it out" button on every endpoint, or /docs/json for the raw OpenAPI spec.
| Endpoint | Returns |
|---|---|
GET /teams, /teams/:id |
Team list / a single team |
GET /teams/:id/interactions, /teams/:id/dependencies, /teams/:id/roles |
Team detail slices |
GET /services, /services/:name |
Service catalog |
GET /search?q= |
Free-text search across teams, services, roles, members |
GET /graph |
The full resolved org graph |
GET /diagrams/topology, /diagrams/hierarchy/:teamId, /diagrams/org-hierarchy |
Diagram data |
GET /context-map |
DDD context map |
GET /cognitive-load, /cognitive-load/:teamId |
Cognitive load assessments |
GET /<domain>, /teams/:id/<domain>, /teams/:id/<domain>/:resourceId |
Any AI-native section: /agents, /memory, /specifications, /steering, /prompts, /playbooks, /policies, /knowledge-base, /workflows, /sessions |
POST /teams/:id/prompts/:promptId/render |
Fill a prompt's {{variable}} placeholders |
POST /context |
Context bundle for a stated goal |
GET /knowledge-graph, /knowledge-graph/:nodeId/traverse |
Knowledge graph traversal |
GET /health |
Health check |
Example: curl http://127.0.0.1:3000/cognitive-load
[
{
"teamId": "platform-payments",
"total": 18,
"label": "elevated",
"assessment": {
"intrinsic": 7,
"extraneous": 5,
"germane": 6,
"notes": "PCI compliance scope adds real intrinsic complexity; onboarding docs need work."
}
},
{
"teamId": "stream-checkout",
"total": 18,
"label": "overloaded",
"assessment": {
"intrinsic": 6,
"extraneous": 8,
"germane": 4,
"notes": "High extraneous load from juggling three upstream integrations (payments, onboarding, fulfillment) with inconsistent contracts; a strong candidate for an anticorruption layer."
}
},
{
"teamId": "stream-onboarding",
"total": 11,
"label": "sustainable",
"assessment": { "intrinsic": 4, "extraneous": 2, "germane": 5, "notes": "Well-bounded domain, low incidental complexity." }
}
]π₯οΈ Dashboard
The same teamapi serve-api also serves a live dashboard at /dashboard β static HTML/CSS/JS fetching the REST API you already have running, no separate process or build step. It shows every team with its type and focus, a cognitive-load bar per team (color- and icon-coded, never color alone), free-text search, and a tabbed diagram viewer (topology / org-hierarchy / context-map) rendered client-side with Mermaid. Each section loads independently, so a blocked CDN (a locked-down corporate network, for instance) only disables the diagram tab β team list, cognitive load, and search keep working.
teamapi serve-api examples/acme-org --port 3000 open http://127.0.0.1:3000/dashboard
π€ MCP tools
teamapi serve-mcp examples/acme-org starts an MCP server you can point Claude Desktop or Claude Code at, then ask about ACME Org like you'd ask a colleague β "who owns checkout-api?", "which team's overloaded?" β no query language needed.
The core tools are list_teams, get_team, get_team_roles, get_team_cognitive_load, find_service_owner, list_services, get_team_interactions, get_team_dependencies, get_context_map, render_org_diagram, search_org, get_org_graph, and get_org_cognitive_load_report. Each AI-native section adds a list_*/get_* pair β list_agents/get_agent, list_prompts/get_prompt, and so on β alongside render_prompt, get_context_bundle, get_knowledge_graph, and traverse_knowledge_graph.
Example: an assistant calling find_service_owner with { "serviceName": "checkout-api" }
{
"teamId": "stream-checkout",
"service": {
"name": "checkout-api",
"versioning": { "type": "semantic" },
"repository": "https://github.com/acme-example/checkout-api",
"boundedContext": {
"ubiquitousLanguage": [
{ "term": "Cart", "definition": "An in-progress, unpaid order" },
{ "term": "Order", "definition": "A cart that has been placed and paid for" }
],
"aggregates": ["Cart", "Order"],
"publishedEvents": ["OrderPlaced"],
"subscribedEvents": ["ChargeAuthorized", "ApplicantActivated"]
}
}
}π¬ Chat
teamapi chat examples/acme-org --team stream-checkout starts an interactive session where the assistant speaks as that team β or, with --member <id>, as one specific person on it. It's backed by a live tool-use loop over the same org-graph operations the MCP server exposes, so it can accurately answer questions about any team, not just its own. Requires ANTHROPIC_API_KEY in your environment; add --debug to see the persona's system prompt and every tool call as it happens.
export ANTHROPIC_API_KEY=sk-ant-...
teamapi chat examples/acme-org --team stream-checkout --member diego-alvesExample:
Chatting as Diego Alves (model: claude-opus-4-8). Type 'exit' or Ctrl+D to quit.
You> is payments overloaded right now?
Diego Alves> Checked Platform Payments' latest self-assessment β they're running "elevated,"
not overloaded. PCI compliance scope is adding real intrinsic load, and their onboarding docs
could use work, but nothing critical right now.
Example, with --debug β every tool call the persona makes, shown inline:
$ teamapi chat examples/acme-org --team stream-checkout --member diego-alves --debug
Chatting as Diego Alves (model: claude-opus-4-8). Type 'exit' or Ctrl+D to quit.
--- system prompt ---
You are Diego Alves (Checkout Tech Lead) on Stream Checkout, a stream-aligned team focused on:
Shopping cart, checkout flow, and order placement.
Use the available tools to look up real facts about this team or any other team in the org β
including cognitive load, services, interactions, and dependencies. Don't guess or make up
specifics; look them up. Stay in character, but you can discuss any team in the org when asked.
---------------------
You> what's the worst thing about your job?
β get_team_cognitive_load({"teamId":"stream-checkout"})
β {
"teamId": "stream-checkout",
"total": 18,
"label": "overloaded",
"assessment": {
"intrinsic": 6,
"extraneous": 8,
"germane": 4,
"notes": "High extraneous load from juggling three upstream integrations (payments,
onboarding, fulfillment) with inconsistent contracts; a strong candidate for an
anticorruption layer."
}
}
Diego Alves> Honestly? We're carrying real extraneous load β three upstream integrations
(payments, onboarding, fulfillment) with inconsistent contracts, so a chunk of my week goes to
translating between them instead of building. Our own self-assessment flags us as "overloaded."
An anticorruption layer would help a lot.
βοΈ Generators
teamapi generate crewai examples/acme-org --out ./crews turns each team into a CrewAI crew β roles become agents, responsibilities become tasks. A responsibility's optional doneWhen becomes that task's expected_output; without one, you get a generic status-report stand-in.
Example: crews/platform-payments/agents.yaml
tech_lead: role: Payments Tech Lead goal: >- Ensure that Payments platform architecture; On-call escalation point. backstory: >- You are the Payments Tech Lead (TechLead) on Platform Payments, which focuses on: Provide payment processing and ledger capabilities as internal platform services. The team owns: payments-api, ledger.
βΆοΈ Running it
crewai create crew acme_payments scaffolds a project with its own config/agents.yaml and config/tasks.yaml β replace those with ours, then wire them up in crew.py:
from crewai import Agent, Crew, Process, Task from crewai.project import CrewBase, agent, crew, task @CrewBase class AcmePaymentsCrew: agents_config = "config/agents.yaml" tasks_config = "config/tasks.yaml" @agent def head_of_engineering(self) -> Agent: return Agent(config=self.agents_config["head_of_engineering"]) @agent def tech_lead(self) -> Agent: return Agent(config=self.agents_config["tech_lead"]) # ...one @agent method per key in agents.yaml @task def tech_lead_task_2(self) -> Task: return Task(config=self.tasks_config["tech_lead_task_2"]) # ...one @task method per key in tasks.yaml @crew def crew(self) -> Crew: return Crew( agents=[self.tech_lead()], # everyone except the manager tasks=self.tasks, process=Process.hierarchical, # this crew's "process" in org.yaml manager_agent=self.head_of_engineering(), # this crew's "managerAgent" ) AcmePaymentsCrew().crew().kickoff()
For a crew org.yaml marks sequential (most of them), skip process/manager_agent entirely β just Crew(agents=self.agents, tasks=self.tasks).
ποΈ Backstage catalog
teamapi generate backstage examples/acme-org --out ./catalog turns the same org graph into a catalog-info.yaml for Backstage: one Group per team (with its members[]), one User per member, and β for any team that owns services[] β a System grouping them plus one Component per service, owned by that team's Group. Drop the file at your catalog's discovery root (or point Backstage's catalog.locations config at it) and it imports directly β no hand-maintained catalog YAML to keep in sync with your org chart.
Example: catalog/catalog-info.yaml (excerpt, --team stream-checkout)
apiVersion: backstage.io/v1alpha1 kind: Group metadata: name: stream-checkout description: Shopping cart, checkout flow, and order placement title: Stream Checkout spec: type: team children: [] members: - diego-alves - yuki-tanaka - fatima-al-sayed --- apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: checkout-api links: - url: https://github.com/acme-example/checkout-api title: Repository spec: type: service lifecycle: production owner: group:stream-checkout system: stream-checkout
Cross-team interactions[]/dependencies[] aren't translated into Backstage's dependsOn relations β those model service-to-service dependencies, and Team API only tracks team-level ones, so guessing a mapping would produce plausible-looking but misleading catalog data. roles[] aren't represented either: Backstage's Group/User model has no concept of a role independent of the person filling it.
π₯ CODEOWNERS
teamapi generate codeowners examples/acme-org --out ./codeowners --org acme writes one CODEOWNERS per repository, so every pull request routes to the team that declared the service. Owners are written as @acme/<team-id> β the same slug teamapi apply provisions β or as members' githubUsername handles when no --org is given.
# Generated by TeamAPI β edit the team's teamapi.yml, not this file.
# Owner: Stream Checkout (stream-checkout)
# Because it owns: checkout-api
* @acme/stream-checkout
Team API is written per team and CODEOWNERS lives per repository, so generating one inverts the index β and that surfaces a question the per-team view hides. A repository claimed by two teams has no correct answer, so none is written and the command exits non-zero:
! acme/checkout-api is claimed by platform-payments and stream-checkout β no CODEOWNERS written
Only the root * rule is emitted: Team API models which team owns a service, not which directories belong to whom. Details in docs/integrations/codeowners.md.
π€ AGENTS.md
teamapi generate agents-md examples/acme-org --out ./agents writes one AGENTS.md per repository, from the team that owns the service in it: who owns this, the bounded context's ubiquitous language, published and subscribed events, the team's policies and steering documents.
# checkout-api β owned by Stream Checkout ## Ubiquitous language - **Cart** β An in-progress, unpaid order - **Order** β A cart that has been placed and paid for
Of every AI integration here this one has the widest reach, precisely because it needs no runtime β no gateway, no server, no adoption decision. Any coding agent that opens the repository reads the file, because that is already the convention. Policies and steering documents are reproduced verbatim, not summarised: an agent reading them is reading what a reviewer would quote back. Details in docs/integrations/agents-md.md.
π’ Port
teamapi generate port examples/acme-org --out ./port emits a Port catalog as blueprints.json (apply once) and entities.json (apply on every change): a teamapi_team per team, a teamapi_service per service related to its owner, and a teamapi_person per member.
It overlaps almost entirely with the Backstage target, with one exception that matters β cognitive load. Port scores and colours numeric properties, so a team's self-assessed load becomes something you can sort by, threshold and alert on. Backstage's entity model has nowhere to put it, so that target drops the most actionable number in the document. Details in docs/integrations/port.md.
π‘ OpenTelemetry
teamapi generate otel examples/acme-org --out ./otel turns ownership into telemetry resource attributes, so a trace, a metric and an alert all know which team to attribute themselves to. service.name and service.namespace are the semantic-convention names; everything org-specific sits under a teamapi. prefix rather than squatting in the reserved namespace.
Two artifacts, because two different people own the levers: one .env per service holding a single OTEL_RESOURCE_ATTRIBUTES line an SDK reads directly, and a collector.yaml transform processor that stamps the same attributes centrally with no deployments touched.
Values are percent-encoded β OTEL_RESOURCE_ATTRIBUTES is W3C Baggage, so a comma in a team name would otherwise truncate the list and silently drop every attribute after it. Details in docs/integrations/opentelemetry.md.
π₯ Import from GitHub
teamapi import github-org <org> --out <dir> bootstraps teamapi.yml files from an org that already exists on GitHub, instead of hand-writing them: one <team-id>/teamapi.yml per GitHub team, with members resolved from GitHub's user profiles (name, email, and the githubUsername that Sync with GitHub teams needs) and a services[] entry per repo the team owns.
$ teamapi import github-org acme-example --out ./imported
Wrote 4 team(s) to ./imported/ β every team defaulted to type: stream-aligned with no roles[]; review and adjust both by hand, then run `teamapi validate`.
GitHub teams carry no Team Topologies typing or role hierarchy, so every generated team defaults to type: stream-aligned with an empty roles[] β both are meant to be corrected by hand, not taken as ground truth. Run teamapi validate ./imported next, then fill in roles[], fix each team's type, and add cognitiveLoad/interactions/dependencies as you would for any hand-authored team. Requires a GitHub token via --token or GITHUB_TOKEN/GH_TOKEN.
π Sync with GitHub teams
Everything above reads the spec; teamapi apply is the one command that writes back to a real system β it reconciles actual GitHub teams and memberships in a GitHub org with what the spec declares, the way terraform plan/apply reconciles infrastructure. One GitHub team per Team API team, matched by slug === team id; members are resolved via each member's githubUsername (add it alongside contact β see the spec). A member with no githubUsername set is reported as skipped, not silently dropped from the plan.
It always prints a plan first. ACME Org's members don't carry a githubUsername (they're fictional), so running it as-is reports every member as skipped; add the field to see adds/removes, e.g.:
$ teamapi apply examples/acme-org --org acme-example
+ create team 'stream-checkout' in acme-example
+ add @diego-alves to 'stream-checkout'
! 'stream-checkout': 2 member(s) skipped, no githubUsername set: yuki-tanaka, fatima-al-sayed
Re-run with --yes to apply this plan.
Nothing is written until you re-run with --yes. A team that doesn't exist yet in GitHub is created (named after the team id, so its slug matches β rename it in GitHub afterward for a friendlier display name); an existing team's membership is diffed and only the difference (adds/removes) is applied. Requires a GitHub token with admin:org scope, via --token or GITHUB_TOKEN/GH_TOKEN.
π» CLI reference
npm install -g @jgalego/teamapi β or pnpm build from a source checkout β puts teamapi on your PATH. If you built with CI=true, which skips linking, run pnpm teamapi <command> ... from the repo root instead.
| Command | Purpose |
|---|---|
teamapi validate <patterns...> |
Resolve every $ref transitively and report unresolved refs |
teamapi render <patterns...> --scope topology|hierarchy|context-map|org-hierarchy [--format mermaid|dot] [--team <id>] [--out <file>] |
Render a diagram |
teamapi scaffold <id> --type <type> [--name <name>] --out <file> |
Generate a minimal, schema-valid document |
teamapi generate crewai|backstage|paperclip|codeowners|agents-md|port|otel <patterns...> [--team <id>] [--company <name>] [--org <org>] --out <dir> |
Generate CrewAI agent/task config, a Backstage catalog-info.yaml, an Agent Companies package, CODEOWNERS, AGENTS.md, a Port catalog, or OpenTelemetry attributes |
teamapi diff <patterns...> --against <ref> |
Diff the resolved org graph against a git revision |
teamapi import github-org <org> --out <dir> [--token <token>] |
Bootstrap teamapi.yml document(s) from an existing GitHub org |
teamapi apply <patterns...> --org <github-org> [--token <token>] [--yes] |
Reconcile GitHub teams/memberships with the org graph (plan by default; --yes executes) |
teamapi slack-sync <patterns...> [--token <token>] [--yes] |
Set each declared Slack channel's topic to name the team that owns it |
teamapi doctor github|slack|pagerduty|okta|paperclip [--token <token>] [--url <url>] [--org <org>] [--company <id>] |
Check a live integration: auth, the read, field shapes, pagination |
teamapi okta-drift <patterns...> --url <url> [--token <token>] [--group-prefix <prefix>] |
Report where declared members and an Okta directory group disagree |
teamapi pagerduty-drift <patterns...> [--token <token>] [--url <url>] |
Report where PagerDuty and the org graph disagree about who gets paged |
teamapi paperclip-drift <patterns...> --url <url> --company <id> [--token <token>] |
Report drift between the org graph and a running Paperclip company (read-only) |
teamapi serve-api <patterns...> [--port 3000] |
Start the read-only REST API |
teamapi serve-mcp <patterns...> |
Start the MCP server |
teamapi chat <patterns...> --team <id> [--member <id>] [--model <id>] [--debug] |
Chat as a team or team member (requires ANTHROPIC_API_KEY) |
<patterns...> accepts file paths, globs, or a directory (auto-discovers every teamapi.yml/.yaml under it).
π°οΈ Org history
Since your org is just files in git, its history is git history. teamapi diff <patterns...> --against <ref> resolves the same patterns two ways β the working tree, and as they existed at any commit, tag, or branch β and reports what changed: teams added/removed, roles/members/services added/removed per team, cognitive-load deltas, and edge changes (interactions, dependencies, cross-team reporting lines). Requires running inside a git repository.
Example, run against this very repo β teamapi diff examples/acme-org --against 931fe6b (the initial commit, before the org-wide role hierarchy was added):
$ teamapi diff examples/acme-org --against 931fe6b
~ platform-payments
+ role added: head-of-engineering
Role edges:
+ reports-to stream-checkout.tech-lead -> platform-payments.head-of-engineering
+ aligns-with stream-checkout.tech-lead -> enabling-devex.coach
+ reports-to stream-onboarding.tech-lead -> platform-payments.head-of-engineering
+ aligns-with stream-onboarding.tech-lead -> enabling-devex.coach
When nothing has changed, it prints a single line rather than an empty report. Either way it exits 0 β this is an inspection tool, not a validation gate (see CI integration for that).
π CI integration
Add JGalego/TeamAPI/.github/actions/validate to a workflow for validation and a diagram preview on every PR that touches your teamapi.yml files, without anyone running the CLI locally:
on: pull_request: paths: ["org/**/teamapi.yml"] permissions: pull-requests: write jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: JGalego/TeamAPI/.github/actions/validate@main with: patterns: org render-scope: topology
It installs @jgalego/teamapi and runs teamapi validate, then posts a single PR comment with the result β kept up to date on later pushes, and carrying a live-rendered Mermaid preview when validation passes. The job fails when validation fails, so it can gate a required check. This repo dogfoods it against examples/acme-org; see .github/workflows/teamapi-preview.yml and the action's inputs and outputs.
π Paperclip
Paperclip orchestrates teams of AI agents β tasks, an org chart for agents, budgets, and a governed tool gateway. TeamAPI and Paperclip both model an organisation, so the division of labour is the important part: TeamAPI declares, Paperclip enforces and executes. AgentSchema.permissions is documented as enforced by external automation rather than by the schema; Paperclip is that automation.
The flow runs one way, spec to runtime. Nothing writes back into teamapi.yml β runtime facts that should inform the spec belong in a pull request, so they stay reviewable.
teamapi serve-mcp examples/acme-org # register with Paperclip's tool gateway teamapi generate paperclip examples/acme-org --out ./company --company "ACME Org" teamapi paperclip-drift examples/acme-org --url http://localhost:3000 --company <id>
Registering the MCP server gives every agent find_service_owner, get_team_cognitive_load, and get_context_bundle as governed tools. generate paperclip emits an agentcompanies/v1 package β a vendor-neutral, git-native markdown format. paperclip-drift reports agents running that nothing declares, declared agents that aren't running, and agents on teams whose policies forbid them; only that last one exits non-zero, so it can gate a required check.
Full mapping, the deliberate gaps, and the suggested loop: docs/integrations/paperclip.md.
π¬ Slack
Every other surface here assumes someone already decided to go and look something up. Slack is where the question actually gets asked. Two halves:
/whoowns as a slash command. Serve the REST API with SLACK_SIGNING_SECRET set and point a Slack command at POST /slack/whoowns:
/whoowns checkout-api
`checkout-api` is owned by *Stream Checkout* (`stream-checkout`).
_Shopping cart, checkout flow, and order placement_
Ask in #stream-checkout.
The route is only registered when the signing secret is set β not "401s when unset", it doesn't exist β so a misconfigured deployment can't expose an unauthenticated endpoint. Signatures are checked in constant time against Slack's v0:<timestamp>:<body> HMAC, and anything older than five minutes is rejected.
Channel topics that name their owner. teamapi slack-sync examples/acme-org prints a plan; --yes applies it:
~ #stream-checkout (stream-checkout)
- (no topic)
+ Stream Checkout β Shopping cart, checkout flow, and order placement Β· Owns: checkout-api
Only topics β not channel creation, invites or archiving. Channels no team declares are counted and left alone, and a channel two teams claim gets nothing, the same call CODEOWNERS makes. Details in docs/integrations/slack.md.
π PagerDuty
Ownership without escalation is half an answer. "Who owns checkout-api" at three in the morning doesn't mean the org chart, it means the rotation β and those two drift apart quietly, because PagerDuty gets edited during an incident and teamapi.yml gets edited in review.
export PAGERDUTY_TOKEN=...
teamapi pagerduty-drift examples/acme-org! unresponsive: 'checkout-api' escalates to 'stream-checkout on-call', which has nobody on it
- unmonitored: 'ledger' is declared by platform-payments but has no PagerDuty service
+ undeclared: 'legacy-batch' is in PagerDuty but no teamapi.yml declares it
~ misattributed: 'payments-api' escalates to 'Default Escalation Policy', which doesn't name platform-payments
4 finding(s), 1 blocking; 2 service(s) matched.
Only unresponsive exits non-zero, so this can gate a required check without ordinary drift failing the build β a monitored service that pages nobody is worse than an unmonitored one, because the alert fires and everyone assumes it was handled. Service names match loosely (Checkout API = checkout-api), and read-only in both directions. Details, and why there is deliberately no generate pagerduty, in docs/integrations/pagerduty.md.
πͺͺ Okta
Every other check here compares the spec to a system the spec is supposed to drive. This one compares it to the only system authoritative over it: people join, move and leave whether or not anyone opens a pull request.
export OKTA_TOKEN=...
teamapi okta-drift examples/acme-org --url https://acme.okta.com! deactivated: 'yuki-tanaka' <yuki.tanaka@acme.example> is DEPROVISIONED in the directory but still listed on stream-checkout
- left: 'noah-fischer' <noah.fischer@acme.example> is declared on stream-onboarding but not in its directory group
+ joined: New Joiner is in stream-checkout's directory group but no member declares them
3 finding(s), 1 blocking; 6 member(s) matched.
Only deactivated exits non-zero. The dangerous finding isn't the missing name, it's the one that's still there β a deactivated account listed as accountable reads to everything downstream, from CODEOWNERS to an agent answering "who owns this", as an owner. Groups match team ids by name (--group-prefix strips a prefix first) and people match by contact email. Read-only: a joiner belongs in a pull request. Details in docs/integrations/okta.md.
π©Ί Checking an integration
Every network integration here degrades silently rather than loudly. A rejected Slack token reads as an empty workspace, so every declared channel comes back missing. An Okta client that stops at page one makes everyone past the first batch look like a leaver β a blocking finding about people who never left.
teamapi doctor slack --token xoxb-β¦ teamapi doctor okta --url https://acme.okta.com teamapi doctor paperclip --url http://localhost:3000 --company acme
slack
β authenticate workspace Acme as teamapi
β list channels 4 channel(s) visible
β channel shape every channel has an id and a name
β pagination followed to 4 item(s) at one per page
All checks passed.
The pagination check asks for one item per page and counts what comes back: getting more than one can only happen if the next page was fetched. With nothing to page through it reports skip, not a pass it hasn't earned. Read-only against every provider, exits 1 on any failure. Details in docs/integrations/doctor.md.
π€ Contributing
See CONTRIBUTING.md for dev setup, everyday commands, and the release process. Security issues: see SECURITY.md rather than filing a public issue.
