TeamAPI latest
On this page
  1. Running it
  2. Backstage catalog
  3. Or: don't generate it at all
  4. CODEOWNERS
  5. AGENTS.md
  6. Port
  7. OpenTelemetry

Generated artifacts

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.

Or: don't generate it at all#

A generated file becomes stale as soon as a team document changes. It stays stale until somebody regenerates it, and repeated gaps erode trust in the catalog.

GET /backstage/catalog serves the same entities live, and @jgalego/teamapi-backstage is a catalog entity provider that polls it:

import { TeamApiEntityProvider } from "@jgalego/teamapi-backstage";

builder.addEntityProvider(
  new TeamApiEntityProvider({ baseUrl: "http://teamapi:3000", token: process.env.TEAMAPI_API_TOKEN }),
);

The catalog stays within one refresh interval of TeamAPI. The provider serves output from the existing generator, so there is no second mapping between the two models to maintain.

The plugin has no @backstage/* dependency. EntityProvider is a structural interface, and depending on the framework to get it would pin this to one Backstage version — the thing most likely to be wrong for any given installation. It applies a full mutation under one stable location, so a team removed from the org graph leaves the catalog too; and a failed refresh leaves the previously ingested entities alone, because a briefly unreachable server is not a reason to empty somebody's service catalog.

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

This AI integration has the widest reach because it needs no gateway, server, or separate adoption decision. Coding agents already read AGENTS.md when they open a repository. Policies and steering documents are reproduced verbatim, giving the agent the same text a reviewer would quote. Details are 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.

The Port and Backstage targets mostly overlap, but Port also carries cognitive load. Port can score and color numeric properties, making a team's self-assessed load sortable and available to thresholds and alerts. Backstage's entity model has nowhere to put that number. supervisionLoad appears as its own property alongside cognitiveLoad because it is not part of the total. You can therefore sort teams by agent-supervision load without reading each YAML file. Details are 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.