All media

Article

The Quote That Knows the Buyer

Chris Carolan
Chris Carolan
Founder & Methodology Lead | The Value-First Team

A HubSpot quote is, by default, a PDF about a transaction. It states a price, asks for a signature, and knows nothing else about the person reading it. That is the wrong shape for a complex sale, where the real risk was never a competitor: it is one person on the buying committee nobody served, going quiet, while a champion who believes in the work has to re-explain that PDF badly to people who were never in the room. It does not have to be the only shape available. On HubSpot's Developer Platform 2026.03, a quote template can be built from custom-coded modules: React components that run inside the quote and read live CRM data at render time. Point that at a real deal instead of a mock, and the quote stops being a document and becomes a web experience that knows the buyer it was built for.

Eight custom modules make that possible, built for HubSpot's own Solutions Partner Quote Template Contest, on a real quote for a real relationship, not a demo dressed up as one. What they do together is the actual story. How I got them working is the price of admission. It's below, but it is not the point.

Eight Modules, One Conversation

Start with the number that would otherwise need a sales conversation to explain: the buyer configures which outcomes they actually need, and the total on the page moves as they do. The outcome set, its prices, and their order all come from Deliverable records already associated to the deal, agreed in discovery, never retyped by a rep into the quote software. A buyer toggles outcomes phrased as their own questions, not a vendor's package names, and watches the number change in front of them.

One of those outcomes does not get a number, on purpose. When a Deliverable's price cannot come from a repeatable pattern, the module renders "Priced after scoping" — no price, no toggle, and the buyer can flag it for a real conversation instead. This is not a copy choice a rep could turn off; the record behind it structurally carries no price, so none can render. A quote that tells a buyer "we will not put a number on this until we've seen your portal, because pricing it blind would be a guess" is doing something almost no proposal does. Every other module on this page is built to show the buyer more. This one shows them less, and says why, in the exact moment a buyer would otherwise never find out a number had been invented.

Above the outcomes sits the conversation itself, rendered back. The discovery session's own notes open the quote: the buyer's account of their situation, in the words they used in the room, not a rep's summary of it. Below that, twelve named complexity patterns render on every quote, and only the ones the session actually confirmed show as observed; the rest render, visibly, as "not observed here." Six of twelve confirmed is a more credible claim on a commercial document than twelve of twelve asserted, and it comes from the session record, not a box a rep remembered to check.

Further down, a phased timeline groups the same Deliverables by the SOW phase they already belong to, with date ranges computed only from dates the records actually carry. Nothing invented to make the timeline look complete. A closing section shows the buyer platform updates a rep chose specifically for their situation, each with the firm's own written take, so the last thing a buyer reads before the terms is the seller being useful about the platform they are about to invest in, not asking for a signature.

None of this is retyped. The outcomes, prices, phases, diagnosis, and updates all come from the deal; a rep configures wording and questions, never content. Where a write-back endpoint exists, toggling an outcome does more than change what a buyer sees: it updates the real line item and syncs the deal amount, which I confirmed with an independent read of the CRM afterward. Without one, the quote still works completely. It simply doesn't persist, and says so rather than pretending it did.

Why It Matters for a Complex Sale

None of this matters for a single-decision-maker sale. It matters for the sale this template was built for, the one named at the top: a complex purchase, decided by a buying committee, where a champion who believes in the work sells internally in rooms the seller is never in.

Hand that champion a static PDF and you have handed them the least useful artifact for that job — a document that states what it costs and nothing about why it fits, that they now have to re-explain badly to a CFO or an IT lead who was never in the discovery conversation. Hand them a quote that already carries the conversation: their own words, the traps actually named, the reasoning behind the price. You have handed them something they can forward as-is and trust to do the explaining for them.

A buying committee needs education, not pressure, and a quote that teaches before it asks for money is the only version of this document actually built for that job.

Buildable Now

This is not a proof of concept sitting in a lab. Every module described above runs on HubSpot's Developer Platform 2026.03, available today to any HubSpot customer who can install a custom-coded quote template, the same capability tier a Solutions Partner or an in-house RevOps team already has access to. Almost nobody in this ecosystem has found it yet. That gap, not the build itself, is the actual reason HubSpot ran a contest for it.

A reader finishing this should not be impressed that one exists. They should be checking whether their own portal has Revenue Hub Professional and whether their own deals already carry the CRM data these eight modules read, because if both are true, which for most consultancies and agencies they already are, this is buildable by them, not just admirable from outside it.

The Price of Admission

None of the above rendered the first time I tried it, and the reason is worth knowing before you start, not after. crm_associations, the function every CRM-reading module in this build depends on, does not return a list: it returns {hasMore, offset, total, results}, and looping over the wrapper instead of .results walks its key names instead of its rows while still reporting a count greater than zero. HubL's {% set %} inside {% for %} does not survive the loop either; the assignment is discarded at the loop's boundary with no error, so any value built by accumulation inside a {% for %} comes out empty. Neither failure throws or logs; both just render nothing where content should be. The fix for both is the same: reshape everything outside HubL, in TypeScript, and never build a value inside a loop that has to survive past it.

formatting=false is required wherever a value does arithmetic: leave it on and a currency property arrives as "$1,800", and Number("$1,800") is NaN. But the same flag flips how a multi-select property crosses the boundary, between an array and a semicolon-delimited string, depending on an argument set elsewhere in the code. Guess the shape wrong and .split(';') on an array throws mid-render and takes the whole module down with it.

A published quote is also a frozen snapshot, not a live view: the HubL runs once, at publish, and nothing associated to the deal after that instant is ever visible on that page. Every piece of CRM data a quote needs has to be attached before it publishes, and testing a code change means publishing a new quote, every time. Refreshing the old one only re-reads the same frozen render.

The diagnostic opener above reads from the deal's Appointment record today, and that is the one dependency worth knowing about before you build on it: Appointments are not readable from CMS HubL at all — a well-formed empty result rather than an error, which I confirmed live against a control that read cleanly in the same render. Neither is Meeting, tested the same way, against a deal I'd independently confirmed carried thirteen meetings. What does read, free and raw, with no association call spent: quoteTemplateContext.deal.<property>, straight off the Deal record itself. That is the actual mechanism behind "the conversation's own words render back" above. It has already shipped, by a different route entirely, in a sibling project: a Deal-record card that reads the same discovery fields through its own serverless function rather than through HubL's association surface at all. Inside this quote template specifically, I've proven the fix live but haven't yet carried it into every module that still reads the older path.

Every claim in this section was checked beside a control that returned a known-present result before I trusted a zero: the discipline that makes "nothing rendered" mean something, instead of just looking like it.

And before debugging a platform from scratch, it's worth checking whether someone in your own organization already has: the correct shape of crm_associations was already written down and verified live, in a different client's project, five weeks before I rediscovered all of it under a contest deadline.

Closing

A quote that reads the CRM creates something before any money moves: it teaches a buyer their own situation back to them, it clarifies what still needs a real conversation instead of a guess, and it gives a champion something they can forward and trust. That is the actual argument for building one — not that it looks better than a PDF, but that it does something a PDF structurally cannot. The capability to build it is already sitting in this ecosystem's own developer platform, unused by almost everyone who has access to it. The next quote like this one does not have to be mine.