Baseline exports
Two exports exist so you can put a number on what delivery costs — without that number ever entering GuideMode.
They are read-only, authenticated endpoints, and each has a matching script for self-hosted deployments. Both produce the same bytes for the same request, so it does not matter which way you take the file.
| Export | Grain | What it is for |
|---|---|---|
| Team baseline | one row per team per month | The measures: delivery, deployment, change effort, AI adoption, perception |
| Team roster | one row per person per team | The join: who is on which team, and under which keys your finance system knows them |
Cost never enters the platform
Section titled “Cost never enters the platform”GuideMode holds no salaries, no day rates and no invoices, and there is no column
anywhere in these files that carries an amount. What the baseline carries is
cost_centre_code — the key you join on, not a cost.
That is a deliberate boundary, not an omission. Cost is accurate where your finance function holds it, and it is allowed to live there; a per-team cost column in a product database is a different artefact with a different legal footing, and adding one “temporarily” is how it stops being a decision anybody made.
So the join happens on your side, in your tooling, on one of:
cost_centre_codeon a team row, when your ledger holds cost by cost centre;cost_centre_codeorexternal_refon a person row in the roster, when it holds cost by employee;supplier_nameon a person row, when supplier cost sits on a purchase order per vendor or named resource.
The roster exists because of that last pair. Finance rarely holds cost at team grain, so without a person-to-team list there is no way to roll a departmental or per-vendor figure up to a team. With one, you do the roll-up yourself, with your own numbers.
The team baseline
Section titled “The team baseline”GET /api/exports/team-baseline?from=YYYY-MM&to=YYYY-MM&format=csv|json| Parameter | Default | Notes |
|---|---|---|
from |
twelve months before to |
Inclusive, YYYY-MM |
to |
the current month | Inclusive, YYYY-MM |
format |
csv |
csv or json |
A window longer than 36 months, or one the wrong way round, is a 400 rather than a
quietly truncated file.
curl -sS -H "Authorization: Bearer $GUIDEMODE_API_KEY" \ "https://app.guidemode.dev/api/exports/team-baseline?from=2026-01&to=2026-12" \ -o team-baseline-2026.csvThe CSV columns, in order:
team_id, month, team_slug, team_name, team_external_ref, cost_centre_code,org_unit_id, org_unit_slug, supplier_name, delivery_model, location_country,headcount, internal_headcount, external_headcount, headcount_by_supplier,active_authors, issues_closed, prs_merged, pr_cycle_time_p50_seconds,issue_lead_time_p50_seconds, production_deploys, change_failure_rate,mttr_p50_seconds, ceu_total, ccv_total, ai_assisted_pr_share, ai_seat_holders,ai_active_users, ai_sessions, ai_commits, survey_response_rate,team_experience_score, aiva_light_overall, aiva_scopeformat=json returns the same field names inside an envelope:
{ "from": "2026-01", "to": "2026-12", "generatedAt": "2026-09-16T09:00:00.000Z", "rows": [ ... ] }Three things to know before you use it:
Every team has a row for every month, including a team that did nothing. A quiet month is zeros, never a missing row — a hole in a monthly series reads as “we did not measure”, which is a different and much worse claim than “nothing happened”.
An empty cell is NULL and means “no denominator”, not zero. Counts (prs_merged,
issues_closed, production_deploys, the AI columns) are genuine counts, so 0 is the
right answer. Rates and percentiles (change_failure_rate, pr_cycle_time_p50_seconds,
ai_assisted_pr_share, survey_response_rate, team_experience_score) are empty when
nothing went into them. A team with no deployment source has no change failure rate; it
does not have a change failure rate of zero. Treat those cells as missing in any average
you build, or you will drag every figure toward zero.
Headcount is what the last register import said, and it repeats unchanged on every
month row. team_members records no join or leave date, so there is no membership history
to report; a historical headcount would be a guess dressed as a measurement. If you need
headcount to move over time, re-import the register and keep the exports.
headcount_by_supplier is a JSON object ({"Acme Consulting": 4}) in both formats — a
string of JSON in the CSV cell, a real object in the JSON one. Note also that
internal_headcount and external_headcount deliberately do not add up to
headcount: contractor and unknown count in the total and in neither split.
The team roster
Section titled “The team roster”GET /api/exports/team-roster?asOf=YYYY-MM-DD&format=csv|json| Parameter | Default | Notes |
|---|---|---|
asOf |
today | Inclusive date stamp. A future date is a 400 |
format |
csv |
csv or json |
curl -sS -H "Authorization: Bearer $GUIDEMODE_API_KEY" \ "https://app.guidemode.dev/api/exports/team-roster" \ -o team-roster.csvThe columns, in order:
external_ref, email, display_name, employment_type, supplier_name,cost_centre_code, location_country, team_slug, team_name, team_external_ref,team_cost_centre_code, org_unit_slug, membership_source, as_ofOne row per (person, team), so somebody on two teams appears twice. Only enabled teams are included.
The roster carries no measures, by design. No pull request counts, no cycle times, no AI usage, not even a date of last activity. That is the whole distinction between a roster and a leaderboard: a file with a person’s name and their throughput beside it becomes a performance document the moment it leaves the product, whatever it was drawn up for — and this one is going to a finance team who did not ask for that and cannot contextualise it. Every value in every row is text. If you need per-person delivery figures, that is a different conversation with different consent, not a column added here.
as_of is an echo, not a filter. Team membership keeps no history, so the roster is
always as it stands right now. asOf is stamped on every row so the file can be filed and
referred to later, and a future date is refused because stamping one would be a claim
about a membership nobody can know yet.
From a self-hosted deployment
Section titled “From a self-hosted deployment”The same query and the same serialisation, run against your own database:
npm run export:team-baseline -- --tenant=<uuid> --from=2026-01 --to=2026-12 \ --format=csv --out=team-baseline-2026.csv
npm run export:team-roster -- --tenant=<uuid> --as-of=2026-09-16 \ --format=csv --out=team-roster.csvOmit --out and the file goes to stdout, so it pipes. Both scripts read DATABASE_URL
from the environment, and both refuse the same windows the API does.
The API key you hand over
Section titled “The API key you hand over”An API key carries the role of the account that created it. A key made by an owner can do everything an owner can do, and these two endpoints require authentication and nothing further, because they are read-only and the key is already bounded by whoever made it.
So the key you give to finance, or paste into a scheduled job, should be created by a member account — not by an owner’s. Create a dedicated user, invite it as a member, and issue the key from there. Revoke it in Settings → API Keys when the engagement ends.
