quoting automation

built, not rolled outclaude code command · python · hubspot

problem
Building a quote meant looking up prices, working out discounts and recurring fees, checking margins, then re-keying it all into the CRM by hand.
what i built
A Claude Code command, /cpq quote, that takes a deal described in plain words. Python prices it, and the quote, line items, deal and contact links are created in HubSpot with the right account manager assigned.
status
It is built and working, but hasn't been rolled out to the team yet, so there are no usage numbers.

demo

>/cpq quote 12 displays with players, mounts and install for Northwind Cafes, 3 years of software and support

cpq · priced 6 line items · drafting the quote in HubSpot

Quote Q-1042

Northwind Cafes · 36-month term

contact
Dana Reyes, operations lead
also involved
Sam Whitfield, finance, signs off the spend
account manager
J. Okafor, assigned to the deal
qtyitemunittotal
12Commercial Display 55in1,200.0014,400.00
12Media Player X1450.005,400.00
12Wall Mount Kit120.001,440.00
12Standard Installation300.003,600.00
volume discount, 10+ screens: 5% of hardware-1,062.00
bundle discount, hardware + software + install: 8% of hardware-1,699.20
one-time total22,078.80
12Content licence, per screen per month25.00300.00
12Pro support, per screen per month15.00180.00
recurring, per month480.00
recurring, over 36 months17,280.00
contract value39,358.80

Needs approval: the 13% hardware discount is over the 10% limit, so this quote went to HubSpot's approval step.

Created in HubSpot: the deal, this quote, 6 line items, and links to the contact and company.

The customer, people, products and prices in this illustration are made up.

how it works

  1. /cpq quote. Someone describes the deal in plain words in Claude Code.
  2. read the request. Claude turns the request into structured inputs: customer, products, quantities and term.
  3. price. Python looks up each product in the price list, applies volume and bundle discounts, and works out the recurring fees.
  4. check limits. The discount and margin are checked against limits. Anything over a limit is marked for approval.
  5. write to hubspot. The deal, quote and line items are created and linked to the right contact and company, and the person responsible for the deal is assigned.
  6. only if over the limit. The quote goes to HubSpot's own approval step.
  7. breakdown. Claude replies with a full breakdown of the deal and everyone involved.

code

pricing.pysimplified rewrite

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal as D, ROUND_HALF_UP
from enum import Enum

Kind = Enum("Kind", "DISPLAY PLAYER MOUNT INSTALL LICENCE SUPPORT")

HARDWARE = {Kind.DISPLAY, Kind.PLAYER, Kind.MOUNT}
BUNDLE = {Kind.DISPLAY, Kind.PLAYER, Kind.INSTALL, Kind.LICENCE}
VOLUME_FROM, VOLUME_RATE = 10, D("0.05")
BUNDLE_RATE = D("0.08")
DISCOUNT_LIMIT = D("0.10")
MARGIN_FLOOR = D("0.20")
# volume + bundle stack to 13%, so big bundles always need approval


def price(lines: dict[str, int], book: dict[str, Item], months: int) -> Quote:
    unknown = sorted(set(lines) - set(book))
    if unknown:
        raise ValueError(f"not in the price list: {', '.join(unknown)}")
    if months <= 0 or any(qty <= 0 for qty in lines.values()):
        raise ValueError("quantities and term must be positive")
    items = [(book[sku], qty) for sku, qty in lines.items()]

    one_off = sum(i.price * q for i, q in items if not i.recurring)
    hardware = sum(i.price * q for i, q in items if i.kind in HARDWARE)
    monthly = sum(i.price * q for i, q in items if i.recurring)
    cost = sum(i.cost * q * (months if i.recurring else 1) for i, q in items)

    discounts = {}
    if sum(q for i, q in items if i.kind is Kind.DISPLAY) >= VOLUME_FROM:
        discounts["volume"] = cents(hardware * VOLUME_RATE)
    if BUNDLE <= {i.kind for i, _ in items}:
        discounts["bundle"] = cents(hardware * BUNDLE_RATE)
    off = sum(discounts.values())

    one_time = one_off - off
    contract = one_time + monthly * months
    quote = Quote(cents(one_time), cents(monthly), cents(contract), discounts)

    # approval itself happens in HubSpot
    if hardware and off / hardware > DISCOUNT_LIMIT:
        quote.approvals.append(Approval("discount", off / hardware, DISCOUNT_LIMIT))
    margin = (contract - cost) / contract if contract else D(0)
    if margin < MARGIN_FLOOR:
        quote.approvals.append(Approval("margin", margin, MARGIN_FLOOR))
    return quote


@dataclass(frozen=True)
class Item:
    sku: str
    kind: Kind
    price: D  # per screen per month if recurring
    cost: D
    recurring: bool = False


@dataclass(frozen=True)
class Approval:
    rule: str
    actual: D
    limit: D


@dataclass
class Quote:
    one_time: D
    monthly: D
    contract_value: D
    discounts: dict[str, D]
    approvals: list[Approval] = field(default_factory=list)


def cents(x) -> D:
    return D(x).quantize(D("0.01"), ROUND_HALF_UP)
A simplified rewrite of the pricing step, not the production code. Run on the demo's order, it returns the quote above, including the approval flag. The products, prices, costs and limits are made up, and the HubSpot write isn't shown.

technical breakdown

from request to quote

CPQ stands for configure, price, quote. The command takes a request the way a salesperson would say it and returns a finished quote plus a breakdown of the deal: the parties and stakeholders, what is being sold, what it costs, and what still needs sign-off.

pricing

Pricing runs in Python: the price-list lookup, volume and bundle discounts, recurring fees, and the margin and approval limits. The same request always produces the same numbers.

approvals

When a quote is over a limit, it goes to HubSpot's own approval step. The automation doesn't add a second approval channel.

what gets written to hubspot

stack

status

It is built and working, at a digital-signage company. It hasn't been rolled out to the team yet, so I have no usage figures or time savings to report. Rolling it out is the next step.

more projects