TeamAPI latest
On this page
  1. Machine-readable output
  2. Versions and migration
  3. Formatting
  4. Project config
  5. Name conflicts
  6. Editor support

CLI and editor

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...> [--format text|json|sarif] Resolve every $ref transitively; report unresolved refs and org-wide name conflicts
teamapi assess <patterns...> [--scan <dir>] [--state <file>] [--format text|json|html|sarif] [--out <file>] Establish and compare an organization-wide accountability baseline
teamapi gaps <patterns...> Report accountability holes between teams — unowned event contracts, vacant seats, unowned agents
teamapi policy <patterns...> Check declared policies against the org graph, and report the ones nothing enforces
teamapi shadow-ai <patterns...> --scan <dir> Report AI adoption found in repositories against what teams declare in agents[]
teamapi render <patterns...> --scope topology|hierarchy|context-map|org-hierarchy [--format mermaid|dot] [--team <id>] [--with-agents] [--out <file>] Render a diagram
teamapi init [dir] [--teams-dir <dir>] [--team <id...>] [--force] Scaffold a whole org repository: config, CI, editor settings, first teams
teamapi scaffold <id> --type <type> [--name <name>] --out <file> Generate a minimal, schema-valid document
teamapi migrate [patterns...] [--check] Bring documents to the latest version, and explain the ones it can't
teamapi fmt [patterns...] [--check] Canonical formatting for Team API documents
teamapi schema [--out <file>] Print the JSON Schema for the document format
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 history <patterns...> [--period commit|day|week|month|quarter] [--since <when>] [--format text|json|csv] Track how the org changed over git history (trends)
teamapi digest <patterns...> [--format text|json|html|slack] [--webhook <url>] [--state <file>] [--out <file>] Summarise findings and what moved, for a schedule (weekly digest)
teamapi diff <patterns...> --against <ref> Diff the resolved org graph against a git revision
teamapi import <source> [argument] --out <dir> [--token <token>] [--url <url>] [--prefix <prefix>] [--match <regex>] Bootstrap teamapi.yml document(s) from GitHub, Backstage, Okta, Slack or a CSV (import)
teamapi apply-to <slack|okta|pagerduty> <patterns...> [--token <token>] [--url <url>] [--prefix <prefix>] [--yes] Reconcile Slack/Okta/PagerDuty membership with the org graph (write back)
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] [--host <host>] [--token <token>] [--cors-origin <origin...>] [--rate-limit <n>] [--allow-anonymous] [--watch] [--reload-endpoint] [--mcp] [--metrics] [--embeddings] [--propose-to <owner/repo>] Start the REST API (exposing it beyond localhost)
teamapi serve-mcp <patterns...> [--watch] Start the MCP server (staying current)
teamapi chat <patterns...> --team <id> [--member <id>] [--provider anthropic|openai] [--model <id>] [--base-url <url>] [--api-key <key>] [--ask <question>] [--quiet] [--debug] Chat as a team or team member, interactively or one-shot (which model)

<patterns...> accepts file paths, globs, or a directory (auto-discovers every teamapi.yml/.yaml under it).

Machine-readable output#

validate, gaps, policy and shadow-ai take --format text | json | sarif; assess also takes html, and diff takes --format text | json.

teamapi gaps examples/driftwood-org --format json | jq '.findings[] | select(.severity == "blocking")'

json emits the report object the library itself returns, not a re-rendering of the text — anything you can do with planGaps's return value in code, you can do here in jq. The text format's "N unresolved reference(s)" warning is suppressed for the structured formats, since a stray line would make the document unparseable for the consumer the format exists for.

sarif is SARIF 2.1.0, which GitHub's code scanning ingests:

- uses: JGalego/TeamAPI/.github/actions/validate@main
  with:
    patterns: teams
    check-gaps: "true"
    sarif-dir: teamapi-sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: teamapi-sarif

That turns every finding into an inline annotation on the pull request diff and an entry in the repository's security tab, with history and dismissal — which is a different thing from a list in a job log. The person who introduced an orphaned event contract sees it on their own diff, at review time, without having gone looking for it.

Paths are emitted relative to the working directory, because SARIF consumers resolve them against the repository root — an absolute path from a CI runner matches no file in the repository and the annotation silently disappears. Severity maps to SARIF's levels on the same line the exit codes draw: blocking becomes error, warning stays warning, info becomes note.

The output format never changes an exit code.

Versions and migration#

teamapi migrate          # bring documents up to the latest teamApiVersion
teamapi migrate --check  # report what needs attention, exit non-zero

There is one version today and no migration to run yet. Building the migration path now avoids designing it under pressure after a second version ships and documents have spread across every repository in the org.

What it does before there are any migrations is tell four situations apart that the schema cannot:

! future/teamapi.yml: This document declares 2.0.0, which is newer than this build understands (1.0.0). Upgrade @jgalego/teamapi rather than changing the document.
! none/teamapi.yml: No teamApiVersion. Add `teamApiVersion: "1.0.0"` — the field is what tells tooling which schema this document follows.
! old/teamapi.yml: No migration path from 0.9.0 to 1.0.0 is registered in this build.

0 file(s) would be migrated, 3 need attention, 1 already current.

The schema reports all three as teamApiVersion: Invalid literal value, expected "1.0.0". That message is accurate but cannot tell an older document from a newer toolchain. Since those cases require opposite actions, both teamapi validate and migrate now provide the version-aware message.

Versions compare numerically rather than lexically, so a format that ever reaches double digits doesn't start telling people their new documents are old.

Formatting#

teamapi fmt          # rewrite documents into canonical form
teamapi fmt --check  # report what would change, write nothing, exit non-zero

Formatting protects review quality. People across the org edit these documents by hand and place new sections according to their own habits. Without a canonical format, two teams making the same change produce unrelated-looking diffs, making reviews harder on files that define accountability.

Top-level keys are ordered the way the schema declares them, not alphabetically: the document reads top to bottom — what this team is, what it owns, who is on it, how it relates to everyone else — and sorting alphabetically would open every file with agents and bury info in the middle. Keys the schema doesn't know stay, after the rest and in their original order, since the format passes unknown fields through and dropping them would be data loss.

Comments survive. A load-and-dump round trip silently deletes every one of them, which across an org means deleting the explanations of why a role reports across a boundary or why a team runs no agents — data loss discovered one file at a time, long after the commit. fmt parses to a comment-preserving document tree instead, so a section that moves takes its commentary with it.

A file that doesn't parse is reported and left alone rather than rewritten on a guess, and one broken document doesn't stop the other forty.

--check is what makes it stick: this repo runs pnpm fmt:check over examples/ in its own verify gate.

Project config#

teamapi.config.yml, found by walking up from the working directory the way git finds .git, so it applies from a repo root, a team's own directory, or a CI checkout one level down:

# Seeds to use when the command line names none.
patterns:
  - org

# Flags that are constant for this org, and were otherwise retyped on every invocation.
defaults:
  github:
    org: acme
  okta:
    url: https://acme.okta.com
    groupPrefix: team-
  serve:
    port: 8080
    corsOrigin: [https://intranet.acme.example]
    rateLimit: 120

gaps: # severity overrides and waivers — see above
  severity:
    unconsumed-event: "off"

topology: # thresholds and severity overrides
  maxTeamSize: 7

With patterns: set, the commands you run dozens of times a day lose their argument entirely:

teamapi gaps          # instead of: teamapi gaps org/**/teamapi.yml
teamapi topology
teamapi serve-api

A command line that names patterns wins and does not merge with the config's — naming patterns is being explicit about scope, and quietly adding the org's default set would resolve teams you didn't ask about. Same precedence for every flag: CLI, then config, then the built-in default.

The schema has no token: field and rejects one if present. The config file lives in the repository, where a convenient token field would invite secret leaks. Every command reads its token from an environment variable.

Parsing is strict throughout: an unknown key, a misspelled section, an unknown gap or topology kind, an out-of-range port — all errors. A waviers: typo that quietly does nothing while you believe a rule is in force is worse than no config at all.

--config <file> points at a specific file; --no-config ignores any.

Name conflicts#

The schema enforces uniqueness within a document — role ids, member ids, agent ids — because that's all one document can see. Some names have to be unique across the whole org, though, because consumers look them up by name alone:

$ teamapi validate org
2 name conflict(s):
  - service 'payments-api' is declared by team-a, team-b — "who owns it" has no single answer
  - channel 'slack:payments' is declared by team-a, team-b — slack-sync would set its topic to whichever ran last

Ask findServiceOwner who owns payments-api when two teams declare it and it answers with whichever team id sorts first — deterministically, and silently. Every consumer inherits that: GET /services/payments-api, the who_owns_service MCP tool, the Slack /whoowns command, generated CODEOWNERS. The other team believes it owns the service and nothing says otherwise.

Queries still need a deterministic result, but sorting cannot settle ownership. Validation reports the ambiguity once and names both claimants, sparing each consumer from rediscovering it. The sorted "winner" is an implementation artifact, not an organizational fact.

Every document in a conflicting org resolves perfectly, which is what makes this different from an unresolved $ref: nothing is missing, and the org is still ambiguous.

Editor support#

Every teamapi.yml in this repo opens with a modeline pointing at the published JSON Schema:

# yaml-language-server: $schema=https://teamapi.dev/schema/v1.json
teamApiVersion: "1.0.0"
id: stream-checkout

That one line gives you completion, hover documentation, and inline validation in any editor running the YAML language server — VS Code (Red Hat YAML extension), Neovim, JetBrains IDEs — with nothing to configure per workspace. teamapi scaffold writes it into every document it generates, so new teams get it for free.

Errors surface as you type rather than at teamapi validate time: an unknown info.type, a misspelled top-level key, a roles[] entry missing its kind.

The schema is generated from the same Zod schemas the resolver validates against — there is no second, hand-maintained copy to drift. Print it yourself with teamapi schema, or write it somewhere with teamapi schema --out schema.json. A test regenerates it and fails if the published copy is stale, so the URL above always matches the code that ships.

For a schema that isn't reachable over the network (air-gapped setups, or pinning a specific version), vendor it into your org's repo and point the modeline at a relative path:

teamapi schema --out .teamapi/schema.json
# yaml-language-server: $schema=../.teamapi/schema.json