Queries · q180 · Head of B2B Commerce · week 1, night 3
“How much of our current B2B wholesale new-business pipeline is already older than the typical window in which deals actually win, i.e. how much open pipeline is quietly turning stale and at risk?”
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 3, request 3 of 3
Night 2's key finding: the stage-history reconstruction came back 30/30 'no transition recorded' — change_history has zero coverage of stage_id changes for these lost opportunities. That's decisive on its own (not a sample-size fluke): the warehouse simply cannot tell us which CRM stage a deal died in, either from current state (always shows the terminal Closed Won/Lost stage) or from history (not logged). I'm treating that as answered-and-closed rather than spending another slot probing it further — it's itself the headline data-quality flag for the memo (reps/system aren't capturing stage progression, which is as much a coaching/hygiene story as a product one). The New-business-only refinement did give a very clean, usable signal instead: lost deals average ~35 days open and ~3.4 activities before dying, vs won deals closing in ~9 days with ~2.3 activities — a ~4x gap in cycle time is a much stronger 'where/why deals die' signal than a stage label would have been, and it directly answers Samuel's staleness question. Tonight's three reports build on that: merchant-level and rep-level win-rate/cycle-time breakdowns (Patrick Q4 / Samuel Q1 — is this widespread or concentrated), and an age-bucket view of the currently open pipeline benchmarked against the won (~9d) vs lost (~35d) cycle-time reference points, to quantify how much live pipeline is already past the point where deals historically still win (Samuel Q3). Holding deal-size/payment-terms correlation (Patrick's optional #5) for night 4 if the memo still has room after seeing tonight's results.
The engineer · 1 dry run on the empty schema, then the submission
WITH fx AS ( SELECT quote_code, rate FROM exchange_rates WHERE base_code = 'EUR' AND as_of = DATE '2025-12-31' ), scoped AS ( SELECT o.id, o.amount, o.currency_code, o.created_at, (DATE '2025-12-31' - CAST(o.created_at AS DATE)) AS age_days FROM opportunities o JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id JOIN tenants t ON t.id = o.tenant_id WHERE o.is_closed = false AND lower(p.name) = 'new business' AND ( lower(t.industry) LIKE '%wholesale%' OR lower(t.industry) LIKE '%industrial%' OR lower(t.industry) LIKE '%office equipment%' ) ), converted AS ( SELECT s.*, CASE WHEN s.currency_code = 'EUR' THEN s.amount ELSE s.amount / fx.rate END AS amount_eur FROM scoped s LEFT JOIN fx ON fx.quote_code = s.currency_code ), bucketed AS ( SELECT c.*, CASE WHEN age_days < 10 THEN '0-9 days' WHEN age_days < 20 THEN '10-19 days' WHEN age_days < 30 THEN '20-29 days' WHEN age_days < 45 THEN '30-44 days' WHEN age_days < 90 THEN '45-89 days' ELSE '90+ days' END AS age_bucket, CASE WHEN age_days < 10 THEN 0 WHEN age_days < 20 THEN 10 WHEN age_days < 30 THEN 20 WHEN age_days < 45 THEN 30 WHEN age_days < 90 THEN 45 ELSE 90 END AS bucket_order FROM converted c ) SELECT age_bucket, COUNT(*) AS opportunities_count, ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_open_pipeline, ROUND(AVG(amount_eur), 2) AS avg_amount_eur, ROUND(SUM(amount_eur), 2) AS total_amount_eur FROM bucketed GROUP BY age_bucket, bucket_order ORDER BY bucket_order
“Submitted. Summary of what the query does:
- Scope: open (is_closed = false) opportunities on each tenant's New business pipeline, restricted to tenants whose industry suggests the B2B wholesale verticals (wholesale/industrial/office equipment).
- Age: 2025-12-31 − created_at, bucketed into 0-9, 10-19, 20-29, 30-44, 45-89, 90+ days, straddling the ~9‑day win and ~35‑day loss reference points.
- Currency: converted to EUR using the 2025-12-31 EUR exchange rate (EUR rows pass through unconverted).
- Output: one row per bucket with count, share of open pipeline, average and total EUR value, ordered youngest to oldest.
One flagged deviation: the request assumed a single pre-established "wholesale tenant," but none was specified here, so I matched tenants by industry text instead of a specific tenant_id — noted in the submit note for the analyst to confirm.”
As Daniel Kim saw it, from the small dataset the agents worked on
age_bucket | opportunities_count | pct_of_open_pipeline | avg_amount_eur | total_amount_eur -----------+---------------------+----------------------+----------------+----------------- (0 rows)
The memo and the queries that followed; links found by title, so inferred
Later queries that build on this report:
It builds on:
Changed only where the benchmark needs the same result on every system
WITH fx AS ( SELECT quote_code, rate FROM exchange_rates WHERE base_code = 'EUR' AND as_of = DATE '2025-12-31'),scoped AS ( SELECT o.id, o.amount, o.currency_code, o.created_at, o.id AS id, o.amount AS amount, o.currency_code AS currency_code, o.created_at AS created_at, (DATE '2025-12-31' - CAST(o.created_at AS DATE)) AS age_days FROM opportunities o JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id JOIN tenants t ON t.id = o.tenant_id WHERE o.is_closed = false AND lower(p.name) = 'new business' AND ( lower(t.industry) LIKE '%wholesale%' OR lower(t.industry) LIKE '%industrial%' OR lower(t.industry) LIKE '%office equipment%' )),converted AS ( SELECT s.*, CASE WHEN s.currency_code = 'EUR' THEN s.amount ELSE s.amount / fx.rate END AS amount_eur ELSE CAST(s.amount / fx.rate AS NUMERIC(18,6)) END AS amount_eur FROM scoped s LEFT JOIN fx ON fx.quote_code = s.currency_code),bucketed AS ( SELECT c.*, CASE WHEN age_days < 10 THEN '0-9 days' WHEN age_days < 20 THEN '10-19 days' WHEN age_days < 30 THEN '20-29 days' WHEN age_days < 45 THEN '30-44 days' WHEN age_days < 90 THEN '45-89 days' ELSE '90+ days' END AS age_bucket, CASE WHEN age_days < 10 THEN 0 WHEN age_days < 20 THEN 10 WHEN age_days < 30 THEN 20 WHEN age_days < 45 THEN 30 WHEN age_days < 90 THEN 45 ELSE 90 END AS bucket_order FROM converted c)SELECT age_bucket, COUNT(*) AS opportunities_count, ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_open_pipeline, ROUND(AVG(amount_eur), 2) AS avg_amount_eur, ROUND(SUM(amount_eur), 2) AS total_amount_eur 100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS pct_of_open_pipeline, AVG(amount_eur) AS avg_amount_eur, SUM(amount_eur) AS total_amount_eurFROM bucketedGROUP BY age_bucket, bucket_orderORDER BY bucket_orderORDER BY bucket_order NULLS LAST
WITH fx AS ( SELECT quote_code, rate FROM exchange_rates WHERE base_code = 'EUR' AND as_of = DATE '2025-12-31' ), scoped AS ( SELECT o.id AS id, o.amount AS amount, o.currency_code AS currency_code, o.created_at AS created_at, (DATE '2025-12-31' - CAST(o.created_at AS DATE)) AS age_days FROM opportunities o JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id JOIN tenants t ON t.id = o.tenant_id WHERE o.is_closed = false AND lower(p.name) = 'new business' AND ( lower(t.industry) LIKE '%wholesale%' OR lower(t.industry) LIKE '%industrial%' OR lower(t.industry) LIKE '%office equipment%' ) ), converted AS ( SELECT s.*, CASE WHEN s.currency_code = 'EUR' THEN s.amount ELSE CAST(s.amount / fx.rate AS NUMERIC(18,6)) END AS amount_eur FROM scoped s LEFT JOIN fx ON fx.quote_code = s.currency_code ), bucketed AS ( SELECT c.*, CASE WHEN age_days < 10 THEN '0-9 days' WHEN age_days < 20 THEN '10-19 days' WHEN age_days < 30 THEN '20-29 days' WHEN age_days < 45 THEN '30-44 days' WHEN age_days < 90 THEN '45-89 days' ELSE '90+ days' END AS age_bucket, CASE WHEN age_days < 10 THEN 0 WHEN age_days < 20 THEN 10 WHEN age_days < 30 THEN 20 WHEN age_days < 45 THEN 30 WHEN age_days < 90 THEN 45 ELSE 90 END AS bucket_order FROM converted c ) SELECT age_bucket, COUNT(*) AS opportunities_count, 100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS pct_of_open_pipeline, AVG(amount_eur) AS avg_amount_eur, SUM(amount_eur) AS total_amount_eur FROM bucketed GROUP BY age_bucket, bucket_order ORDER BY bucket_order NULLS LAST
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: Age Profile of Currently Open New-Business Pipeline Question: How much of our current B2B wholesale new-business pipeline is already older than the typical window in which deals actually win, i.e. how much open pipeline is quietly turning stale and at risk? Grain: one row per age bucket of currently-open opportunities Columns: - age_bucket - opportunities_count - pct_of_open_pipeline - avg_amount_eur - total_amount_eur Details: Scope: same wholesale tenant filter, opportunities where pipeline_id = the tenant's 'New business' pipeline and is_closed = false (currently open), as of the last complete data date 2025-12-31 (use the fixed literal date '2025-12-31' as the reference point for age, not CURRENT_DATE/now(), per house SQL standards). age_days = 2025-12-31 minus created_at. Bucket into: '0-9 days', '10-19 days', '20-29 days', '30-44 days', '45-89 days', '90+ days' (chosen because last night's New-business-only comparison showed won deals close in ~9 days on average and lost deals take ~35 days on average before dying, so these buckets straddle both reference points). Convert amount to EUR using exchange_rates as of 2025-12-31. pct_of_open_pipeline = that bucket's opportunity count / total open opportunities in scope, as a percentage. total_amount_eur = sum of amount_eur in that bucket (euro value of pipeline sitting at that age). Order by age_bucket ascending (by lower day bound). Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH fx AS ( SELECT quote_code, rate FROM exchange_rates WHERE base_code = 'EUR' AND as_of = DATE '2025-12-31' ), scoped AS ( SELECT o.id, o.amount, o.currency_code, o.created_at, (DATE '2025-12-31' - CAST(o.created_at AS DATE)) AS age_days FROM opportunities o JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id JOIN tenants t ON t.id = o.tenant_id WHERE o.is_closed = false AND lower(p.name) = 'new business' AND ( lower(t.industry) LIKE '%wholesale%' OR lower(t.industry) LIKE '%industrial%' OR lower(t.industry) LIKE '%office equipment%' ) ), converted AS ( SELECT s.*, CASE WHEN s.currency_code = 'EUR' THEN s.amount ELSE s.amount / fx.rate END AS amount_eur FROM scoped s LEFT JOIN fx ON fx.quote_code = s.currency_code ), bucketed AS ( SELECT c.*, CASE WHEN age_days < 10 THEN '0-9 days' WHEN age_days < 20 THEN '10-19 days' WHEN age_days < 30 THEN '20-29 days' WHEN age_days < 45 THEN '30-44 days' WHEN age_days < 90 THEN '45-89 days' ELSE '90+ days' END AS age_bucket, CASE WHEN age_days < 10 THEN 0 WHEN age_days < 20 THEN 10 WHEN age_days < 30 THEN 20 WHEN age_days < 45 THEN 30 WHEN age_days < 90 THEN 45 ELSE 90 END AS bucket_order FROM converted c ) SELECT age_bucket, COUNT(*) AS opportunities_count, ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_open_pipeline, ROUND(AVG(amount_eur), 2) AS avg_amount_eur, ROUND(SUM(amount_eur), 2) AS total_amount_eur FROM bucketed GROUP BY age_bucket, bucket_order ORDER BY bucket_order
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: age_bucket, opportunities_count, pct_of_open_pipeline, avg_amount_eur, total_amount_eur
submitWITH fx AS ( SELECT quote_code, rate FROM exchange_rates WHERE base_code = 'EUR' AND as_of = DATE '2025-12-31' ), scoped AS ( SELECT o.id, o.amount, o.currency_code, o.created_at, (DATE '2025-12-31' - CAST(o.created_at AS DATE)) AS age_days FROM opportunities o JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id JOIN tenants t ON t.id = o.tenant_id WHERE o.is_closed = false AND lower(p.name) = 'new business' AND ( lower(t.industry) LIKE '%wholesale%' OR lower(t.industry) LIKE '%industrial%' OR lower(t.industry) LIKE '%office equipment%' ) ), converted AS ( SELECT s.*, CASE WHEN s.currency_code = 'EUR' THEN s.amount ELSE s.amount / fx.rate END AS amount_eur FROM scoped s LEFT JOIN fx ON fx.quote_code = s.currency_code ), bucketed AS ( SELECT c.*, CASE WHEN age_days < 10 THEN '0-9 days' WHEN age_days < 20 THEN '10-19 days' WHEN age_days < 30 THEN '20-29 days' WHEN age_days < 45 THEN '30-44 days' WHEN age_days < 90 THEN '45-89 days' ELSE '90+ days' END AS age_bucket, CASE WHEN age_days < 10 THEN 0 WHEN age_days < 20 THEN 10 WHEN age_days < 30 THEN 20 WHEN age_days < 45 THEN 30 WHEN age_days < 90 THEN 45 ELSE 90 END AS bucket_order FROM converted c ) SELECT age_bucket, COUNT(*) AS opportunities_count, ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_open_pipeline, ROUND(AVG(amount_eur), 2) AS avg_amount_eur, ROUND(SUM(amount_eur), 2) AS total_amount_eur FROM bucketed GROUP BY age_bucket, bucket_order ORDER BY bucket_order
Submitted to tonight's batch. Your work on this request is done.