Win/loss analysis
Generate hierarchy-scoped win/loss reports from recorded deal outcomes, extract recurring themes and competitors, and surface battlecard suggestions.
Win/loss analysis
Win/loss turns per-deal outcome capture into narrative reports a sales leader
actually reads — recurring themes, competitor mentions, buyer decision criteria,
and the pipeline stage where lost deals dropped off. Reports are scoped to the
caller's management hierarchy so a rep never sees a peer's deals through the
report. The API is under /v1/win-loss and is gated by the analytics module.
Live
Backed by the win_loss_reports and win_loss_report_deals tables plus the
per-deal win_loss_records capture. Generation, theme extraction, and
hierarchy scoping are all real code (core.win_loss).
What a report is
POST /v1/win-loss/generate (admin/owner only) pulls a bounded set of deal
outcomes and produces one win_loss_reports row. Inputs:
period_start/period_end— bounded againstwin_loss_records.recorded_at.segment_filter— an optional dict supportingsegment(substring match on notes),owner_emails, andmin_value/max_valuebounds on deal value.
Generation is idempotent: the unique index over
(tenant_id, period_start, period_end, segment_filter) means re-running the same
triple updates the existing row in place rather than spawning a duplicate. Each
re-run recomputes the theme block from scratch, since the underlying deal set can
grow between runs.
Theme extraction
core.win_loss.theme_extractor produces the four blocks stamped onto
win_loss_reports.ai_themes_json:
themes— recurring short noun phrases across dealscompetitors— competitor mentions with frequency and sharedecision_criteria— buyer-stated criteria recurring in notesdrop_off_stages— for lost deals, the stage where the deal was lost
Extraction has two paths. When the tenant has an AI provider configured, it
routes through the tenant-scoped AIClient (credential + ledger + budget +
kill-switch + PII redaction) with a strict-JSON prompt. When no key is present —
CI, dev, or a tenant that hasn't connected a provider — it falls through to a
deterministic keyword-bucketing heuristic tagged heuristic-v1, so the detail
page always renders something concrete.
Hierarchy scoping
Report contents are constrained by core.visibility.hierarchy —
specifically owner_email_filter_clause. The report header is a tenant-scoped
artifact, but the deals inside it are filtered to the viewer's subtree. Default-
deny applies: a viewer with no subordinates and no admin role sees a report
header only when they personally recorded at least one of its deals. The same
filter is applied on both the list and the drill-down, so a rep can't reach peer
deals through GET /v1/win-loss/reports/{id}.
Endpoints
| Method & path | Purpose |
|---|---|
GET /v1/win-loss | Per-deal feed for the caller |
POST /v1/win-loss/generate | Generate a report (admin/owner) |
GET /v1/win-loss/reports | Paginated report list, hierarchy-filtered |
GET /v1/win-loss/reports/{id} | Full detail with scoped deal drill-down |
Battlecards
When a competitor recurs across reports, core.win_loss.battlecards.suggest_battlecards
aggregates the competitor and decision-criteria blocks and emits deterministic
suggestion records — "Competitor X appears in 5 of the last 8 reports, price
cited each time." This is the statistical signal to go build a battlecard, not
a generated talk track; suggestions are sorted by descending total mentions and
filtered by minimum-mention/minimum-report thresholds so a single stray mention
doesn't create noise.
These suggestions are not stored and are not the battlecards themselves. The
battlecards live on the Sales Engineering workspace's Battlecards page
(/se/battlecards, API /v1/se/battlecards), and these win/loss themes are one of
their inputs:
- Generate a card for a named competitor. Pact assembles it from consent-cleared wins in the win vault and from these win/loss themes.
- Version history. Every regeneration is a new version with a structured delta, so you can see what changed and why.
- Publish to Slack. Before publishing, Pact re-checks the consent of every cited quote. If a citation's consent was revoked, the publish is blocked rather than quietly dropping the quote.
Generated, not written
Battlecards are assembled deterministically from your data. There is no LLM talk track and no free-text editor. To change a card, change the evidence behind it and regenerate.