Queries · q174 · Head of B2B Commerce · week 1, night 1
“Among closed B2B wholesale opportunities, what reliably differs between the ones we won and the ones we lost — deal size, touches/activities before close, days open, or how concentrated they are among accounts and reps?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 1; shared with Samuel Whitcombe, Head of Merchant Success
B2B merchants say their sales teams struggle. How do their pipelines convert from lead to won deal, how long does it take, and what predicts winning or losing?
The manager, at the start of week 1
Note to Daniel Kim — B2B pipeline health
Daniel,
Samuel and I need to give the B2B wholesale merchants (and our own leadership) a clear-eyed view of why their sales teams are struggling to close business. This feeds a joint recommendation Samuel and I are making next week on where to invest — better pipeline coaching/tooling for merchants vs. changes to how we structure stages, quotes and terms in the product. So I want the picture to be accurate more than exhaustive — pick your battles across the nights you have.
Here's what I actually want to know, in plain terms:
Priority order is 1, 2, 3 first — those directly shape the recommendation. 4 and 5 are valuable but can slip if you run out of slots this week.
Keep each report to one clean question — I'd rather have four sharp answers than one crowded one. Flag anything that looks like a data quality issue rather than a real trend.
Thanks — looking forward to the Friday memo.
Patrick
Subject: A few questions from the Merchant Success side
Hi Daniel,
Good to see this getting attention this week. A few things I'd love your analysis to help answer, from the merchant-health angle:
Happy to compare notes before you finalize anything for Patrick.
Best, Samuel
The analyst · week 1, night 1, request 3 of 3
Night 1: going straight at Patrick's top three priorities (graveyard stage, cycle-time trend, won-vs-lost drivers), which also cover most of Samuel's three questions (stage friction, what separates wins/losses, implicitly sets up stale-pipeline-risk follow-up for night 2). All three reports scope to B2B wholesale tenants via an industry-text heuristic (industrial supplies / office equipment / wholesale) since there's no explicit 'B2B wholesaler' flag on tenants — I've asked the report to surface the merchant list so I can sanity-check the filter caught the right handful of tenants before going further. Kept each report to one grain: (tenant x stage) for the funnel, (month) for cycle time, and (won/lost) for the comparison, per the one-question-one-report rule. Held back Samuel's 'stale pipeline = risk' question and Patrick's #4 (merchant/rep dispersion) and #5 (terms/size correlation) for later nights once I see where the real friction shows up — no point spending slots on dispersion or terms analysis if the stage report shows everyone dying at the same place for the same reason.
The engineer · 1 dry run on the empty schema, then the submission
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id, o.tenant_id, o.account_id, o.owner_user_id, o.is_won, o.closed_at, o.created_at, o.amount, o.currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= o.closed_at::date ORDER BY er.as_of DESC LIMIT 1 ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, ROUND(AVG(co.amount_eur), 2) AS avg_amount_eur, ROUND(CAST(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS numeric), 2) AS median_amount_eur, ROUND(AVG(ac.activity_count), 2) AS avg_activities_before_close, ROUND(CAST(AVG(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) / 86400.0) AS numeric), 1) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC
“Query compiles and returns exactly the requested columns and grain. Submitting.”
“Submitted. Summary of the approach:
- Scoped to B2B wholesale tenants by industry pattern match, then to opportunities with is_closed = true, split into won/lost by is_won.
- Converted each opportunity's amount to EUR using the exchange_rates rate (base EUR → opportunity currency) as of closed_at's date, falling back to the nearest prior available date to handle weekends/gaps.
- Computed per-opportunity activity counts (activities linked by opportunity_id with occurred_at <= closed_at), then averaged within each outcome group.
- Days open = closed_at - created_at averaged in days.
- Distinct accounts/reps counted via COUNT(DISTINCT account_id) / COUNT(DISTINCT owner_user_id) per group.
- One row per outcome (won, lost), ordered won first.”
As Daniel Kim saw it, from the small dataset the agents worked on
outcome | opportunities | avg_amount_eur | median_amount_eur | avg_activities_before_close | avg_days_open | distinct_accounts | distinct_reps --------+---------------+----------------+-------------------+-----------------------------+---------------+-------------------+-------------- won | 339 | 5736.76 | 4500.00 | 3.0 | 18.9 | 155 | 10 lost | 67 | 4946.74 | 4200.00 | 3.48 | 33.1 | 67 | 9 (2 rows)
The memo and the queries that followed; links found by title, so inferred
Later queries that build on this report:
Changed only where the benchmark needs the same result on every system
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%'),closed_opps AS ( SELECT o.id, o.tenant_id, o.account_id, o.owner_user_id, o.is_won, o.closed_at, o.created_at, o.amount, o.currency_code, SELECT o.id AS id, o.tenant_id AS tenant_id, o.account_id AS account_id, o.owner_user_id AS owner_user_id, o.is_won AS is_won, o.closed_at AS closed_at, o.created_at AS created_at, o.amount AS amount, o.currency_code AS currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= o.closed_at::date ORDER BY er.as_of DESC LIMIT 1 AND er.as_of <= CAST(o.closed_at AS date) ORDER BY er.as_of DESC NULLS FIRST {{fetch_first(1)}} ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL),activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id)SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, ROUND(AVG(co.amount_eur), 2) AS avg_amount_eur, ROUND(CAST(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS numeric), 2) AS median_amount_eur, ROUND(AVG(ac.activity_count), 2) AS avg_activities_before_close, ROUND(CAST(AVG(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) / 86400.0) AS numeric), 1) AS avg_days_open, AVG(co.amount_eur) AS avg_amount_eur, {{percentile_cont(0.5, co.amount_eur)}} AS median_amount_eur, AVG(CAST(ac.activity_count AS NUMERIC(18,6))) AS avg_activities_before_close, AVG({{seconds_between(co.created_at, co.closed_at)}} / 86400.0) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_repsFROM closed_opps coJOIN activity_counts ac ON ac.opportunity_id = co.idGROUP BY co.is_wonORDER BY co.is_won DESCORDER BY co.is_won DESC NULLS FIRST
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id AS id, o.tenant_id AS tenant_id, o.account_id AS account_id, o.owner_user_id AS owner_user_id, o.is_won AS is_won, o.closed_at AS closed_at, o.created_at AS created_at, o.amount AS amount, o.currency_code AS currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= CAST(o.closed_at AS date) ORDER BY er.as_of DESC NULLS FIRST FETCH FIRST 1 ROWS ONLY ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, AVG(co.amount_eur) AS avg_amount_eur, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS median_amount_eur, AVG(CAST(ac.activity_count AS NUMERIC(18,6))) AS avg_activities_before_close, AVG(CAST(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) AS NUMERIC(18,6)) / 86400.0) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC NULLS FIRST
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id AS id, o.tenant_id AS tenant_id, o.account_id AS account_id, o.owner_user_id AS owner_user_id, o.is_won AS is_won, o.closed_at AS closed_at, o.created_at AS created_at, o.amount AS amount, o.currency_code AS currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= CAST(o.closed_at AS date) ORDER BY er.as_of DESC NULLS FIRST LIMIT 1 ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, AVG(co.amount_eur) AS avg_amount_eur, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS median_amount_eur, AVG(CAST(ac.activity_count AS NUMERIC(18,6))) AS avg_activities_before_close, AVG(CAST(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) AS NUMERIC(18,6)) / 86400.0) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC NULLS FIRST
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id AS id, o.tenant_id AS tenant_id, o.account_id AS account_id, o.owner_user_id AS owner_user_id, o.is_won AS is_won, o.closed_at AS closed_at, o.created_at AS created_at, o.amount AS amount, o.currency_code AS currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= CAST(o.closed_at AS date) ORDER BY er.as_of DESC NULLS FIRST FETCH FIRST 1 ROWS ONLY ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, AVG(co.amount_eur) AS avg_amount_eur, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY CAST(co.amount_eur AS DOUBLE PRECISION)) AS median_amount_eur, AVG(CAST(ac.activity_count AS NUMERIC(18,6))) AS avg_activities_before_close, AVG(CAST(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) AS NUMERIC(18,6)) / 86400.0) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC NULLS FIRST
WITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id AS id, o.tenant_id AS tenant_id, o.account_id AS account_id, o.owner_user_id AS owner_user_id, o.is_won AS is_won, o.closed_at AS closed_at, o.created_at AS created_at, o.amount AS amount, o.currency_code AS currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= CAST(o.closed_at AS date) ORDER BY er.as_of DESC NULLS FIRST FETCH FIRST 1 ROWS ONLY ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, AVG(co.amount_eur) AS avg_amount_eur, quantileExactInclusive(0.5)(toFloat64(co.amount_eur)) AS median_amount_eur, AVG(CAST(ac.activity_count AS NUMERIC(18,6))) AS avg_activities_before_close, AVG(CAST(divideDecimal(toDecimal128(dateDiff('microsecond', co.created_at, co.closed_at), 0), toDecimal128(1000000, 0), 6) AS Nullable(Decimal(18, 6))) / 86400.0) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC NULLS FIRST
The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted
Report request from Daniel Kim (analyst for the Head of B2B Commerce): Title: B2B Wholesale Opportunities: Won vs Lost Characteristics Question: Among closed B2B wholesale opportunities, what reliably differs between the ones we won and the ones we lost — deal size, touches/activities before close, days open, or how concentrated they are among accounts and reps? Grain: one row per closed outcome (won or lost) Columns: - outcome (won / lost) - opportunities (count) - average amount in EUR - median amount in EUR - average activities per opportunity before close - average days open (closed_at - created_at) - distinct accounts involved - distinct owning reps (owner_user_id) involved Details: Scope to the same B2B wholesale tenants as the other two reports (industry ILIKE '%industrial%' OR '%office equipment%' OR '%wholesal%'). Use opportunities where is_closed = true, split into is_won = true (won) vs is_won = false (lost), across all history through 2025-12-31. Convert opportunity amount to EUR using exchange_rates (base_code='EUR', quote_code = opportunity currency_code, rate as_of the opportunity's closed_at date, or nearest prior business day if a weekend) before averaging across tenants/currencies, per house standards. 'Activities before close' = count of rows in activities where activities.opportunity_id = opportunities.id and activities.occurred_at <= opportunities.closed_at, averaged per opportunity within each outcome group. Distinct accounts/reps are counts of distinct account_id / owner_user_id among opportunities in that outcome group. Order rows with 'won' first, then 'lost'. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id, o.tenant_id, o.account_id, o.owner_user_id, o.is_won, o.closed_at, o.created_at, o.amount, o.currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= o.closed_at::date ORDER BY er.as_of DESC LIMIT 1 ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, ROUND(AVG(co.amount_eur), 2) AS avg_amount_eur, ROUND(CAST(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS numeric), 2) AS median_amount_eur, ROUND(AVG(ac.activity_count), 2) AS avg_activities_before_close, ROUND(CAST(AVG(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) / 86400.0) AS numeric), 1) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: outcome, opportunities, avg_amount_eur, median_amount_eur, avg_activities_before_close, avg_days_open, distinct_accounts, distinct_reps
submitWITH b2b_tenants AS ( SELECT id FROM tenants WHERE industry ILIKE '%industrial%' OR industry ILIKE '%office equipment%' OR industry ILIKE '%wholesal%' ), closed_opps AS ( SELECT o.id, o.tenant_id, o.account_id, o.owner_user_id, o.is_won, o.closed_at, o.created_at, o.amount, o.currency_code, CASE WHEN o.currency_code = 'EUR' THEN o.amount ELSE o.amount / ( SELECT er.rate FROM exchange_rates er WHERE er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of <= o.closed_at::date ORDER BY er.as_of DESC LIMIT 1 ) END AS amount_eur FROM opportunities o JOIN b2b_tenants t ON t.id = o.tenant_id WHERE o.is_closed = true AND o.closed_at IS NOT NULL ), activity_counts AS ( SELECT co.id AS opportunity_id, COUNT(a.id) AS activity_count FROM closed_opps co LEFT JOIN activities a ON a.opportunity_id = co.id AND a.occurred_at <= co.closed_at GROUP BY co.id ) SELECT CASE WHEN co.is_won THEN 'won' ELSE 'lost' END AS outcome, COUNT(*) AS opportunities, ROUND(AVG(co.amount_eur), 2) AS avg_amount_eur, ROUND(CAST(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY co.amount_eur) AS numeric), 2) AS median_amount_eur, ROUND(AVG(ac.activity_count), 2) AS avg_activities_before_close, ROUND(CAST(AVG(EXTRACT(EPOCH FROM (co.closed_at - co.created_at)) / 86400.0) AS numeric), 1) AS avg_days_open, COUNT(DISTINCT co.account_id) AS distinct_accounts, COUNT(DISTINCT co.owner_user_id) AS distinct_reps FROM closed_opps co JOIN activity_counts ac ON ac.opportunity_id = co.id GROUP BY co.is_won ORDER BY co.is_won DESC
Submitted to tonight's batch. Your work on this request is done.