PluginBench
Skill
Pass
Audit score 90

quote-to-cash

hubspot/agent-cli-skills

Build product catalogs, assemble quotes with line items, and track invoices and subscriptions through revenue.

What is quote-to-cash?

Manage the quote-to-cash workflow in HubSpot: create and organize products, build quotes with associated line items and deals, and monitor invoices and subscriptions. Use this when you need to handle product catalogs, generate quotes, or track billing and revenue status.

  • Create and manage product catalogs with pricing and SKUs
  • Build quotes with line items and associate them to deals
  • Track outstanding, overdue, and paid invoices by status and date
  • Monitor active and past-due subscriptions with MRR/ARR metrics
  • Bulk import products and perform batch operations on quotes and line items
  • Update quote status and discount handling

How to install quote-to-cash

npx skills add https://github.com/hubspot/agent-cli-skills --skill quote-to-cash
Prerequisites
  • HubSpot CLI installed and authenticated with appropriate OAuth or private-app token
  • Read scope for invoices, subscriptions, orders, or carts if querying those object types
  • Familiarity with bulk-operations skill for batch workflows
Claude Code
Cursor
Windsurf
Cline

How to use quote-to-cash

  1. 1.Create products using `hubspot objects create --type products` with name, price, and SKU properties
  2. 2.Build line items from a JSONL file and pipe them to `hubspot objects create --type line_items`
  3. 3.Create a quote with `hubspot objects create --type quotes` and capture its ID
  4. 4.Associate line items to the quote and the quote to a deal using `hubspot associations create`
  5. 5.Search invoices by status (OUTSTANDING, PAID) and due date using `hubspot objects search --type invoices`
  6. 6.Query subscriptions by status and calculate MRR totals with jq filtering
  7. 7.Update quote status to APPROVAL_NOT_NEEDED when ready to share

Use cases

Good for
  • Generate a quote for a deal by creating line items, assembling them into a quote, and linking to the deal record
  • Find all outstanding invoices due in the next 30 days to prioritize collections
  • Calculate total monthly recurring revenue (MRR) across active subscriptions
  • Bulk-import a product catalog from a spreadsheet into HubSpot
  • Track overdue invoices and flag accounts for follow-up
Who it's for
  • Sales operations and quote management teams
  • Finance and billing administrators
  • Revenue operations professionals
  • Deal managers building customer proposals

quote-to-cash FAQ

How do I create a quote with multiple line items?

Create line items first by piping JSONL to `hubspot objects create --type line_items`, capture their IDs, then create the quote and associate each line item to it using `hubspot associations create`.

Why am I getting a 403 error when querying invoices or subscriptions?

The active token (OAuth or private-app) is missing the required read scope (invoices-read, subscriptions-read, etc.). Check your token permissions in HubSpot.

Can I set the total discount on a line item directly?

No, `hs_total_discount` is read-only. Set the `discount` property as a percentage (e.g., 10 for 10% off) instead.

How do I verify enum values for properties like hs_status or hs_invoice_status?

Run `hubspot properties get --type <type> --name <property> --format json | jq -r '.options[].value'` to see all valid values for your portal.

Can I create invoices from the CLI?

No, invoice creation typically requires HubSpot Commerce and the UI. The CLI can read and update invoice status, but not create them from scratch.

Full instructions (SKILL.md)

Source of truth, from hubspot/agent-cli-skills.


name: quote-to-cash description: Build the product catalog, assemble quotes (line items + associations to deals), and track invoices and subscriptions through to revenue. triggers:

  • "create product"
  • "product catalog"
  • "build a quote"
  • "create quote"
  • "line items"
  • "associate line items"
  • "outstanding invoices"
  • "overdue invoices"
  • "active subscriptions"
  • "quote to cash"

Resources

FileWhen to use
resources/q2c-essentials.mdSix-field cheat sheet, association directions, portal caveats for invoices/subscriptions/orders/carts.

Foundations

Read bulk-operations/SKILL.md first — JSONL piping, batch read, pagination, and the dry-run/digest/confirm flow for destructive ops live there. Reshape recipes (read → write payload) are in bulk-operations/resources/json-patterns.md.

hubspot <command> --help is the source of truth. Object types are plural (products, line_items, quotes, invoices, subscriptions). Never hardcode property tables — hubspot properties list --type <type> is one call away. Verify any enum value the agent is about to write with hubspot properties get --type <type> --name <property> and read options[].value.

Portal note: invoices, subscriptions, orders, carts show an empty objectTypeId in hubspot objects types. They work through objects search/list when the token has the matching scope (invoices-read, subscriptions-read, etc.) and 403 otherwise. CLI-created quotes are always DRAFT; approval routing, share links, PDF generation, and invoice creation usually require the HubSpot UI.

1. Create a product

hubspot objects create --type products \
  --property name="Enterprise License" \
  --property price=12000 \
  --property hs_sku=ENT-001

For a recurring product set recurringbillingfrequency; check the API enum values first with hubspot properties get --type products --name recurringbillingfrequency --format json | jq -r '.options[].value'. Bulk-import a catalog by piping JSONL of {"properties":{...}} to hubspot objects create --type products --dry-run.

2. Build a quote: line items → quote → associations

objects create emits one result line per stdin line, in input order. That lets you build line items, capture their IDs, and associate them to the new quote in three pipes — no per-record shell loop.

DEAL_ID=12345

# 1. Create the line items. items.jsonl holds {"name":..,"qty":..,"price":..,"product_id":..} per line.
jq -c '{properties:{
    name:.name, quantity:(.qty|tostring), price:(.price|tostring),
    hs_product_id:.product_id, hs_line_item_currency_code:"USD"
  }}' items.jsonl \
| hubspot objects create --type line_items > /tmp/lineitems.jsonl

# 2. Create the quote.
QUOTE_ID=$(hubspot objects create --type quotes \
  --property hs_title="Acme Corp - 2026" \
  --property hs_expiration_date=2026-06-30 \
  --property hs_currency=USD \
  --format json | jq -r '.data.id // .id')

# 3. Associate every new line item to the quote in one pipe.
jq -r '.id' /tmp/lineitems.jsonl \
| jq -cR --arg q "$QUOTE_ID" '{from:("quotes:" + $q), to:("line_items:" + .)}' \
| hubspot associations create

# 4. Link the quote to the deal.
hubspot associations create --from "deals:$DEAL_ID" --to "quotes:$QUOTE_ID"

Discount handling — discount is the writable percentage (10 = 10% off). hs_total_discount is HubSpot-computed; do not set it. Verify with hubspot properties get --type line_items --name hs_total_discount (look for modificationMetadata.readOnlyValue:true) before relying on this in a portal you don't own.

Promote a quote out of DRAFT when ready to share:

hubspot objects update --type quotes <quote_id> --property hs_status=APPROVAL_NOT_NEEDED

Verify hs_status enum values for your portal: hubspot properties get --type quotes --name hs_status --format json | jq -r '.options[].value'.

3. Track invoices

The CLI reads invoice data and updates status; creation usually needs HubSpot Commerce + UI. Filter by hs_invoice_status and date.

# All outstanding invoices
hubspot objects search --type invoices \
  --filter "hs_invoice_status=OUTSTANDING" \
  --properties hs_number,hs_amount_billed,hs_balance,hs_due_date

# Past-due (overdue) invoices, dynamic date
hubspot objects search --type invoices \
  --filter "hs_due_date<$(date +%Y-%m-%d) AND hs_invoice_status!=PAID" \
  --properties hs_number,hs_due_date,hs_balance

# Invoices billed in the last 30 days
hubspot objects search --type invoices \
  --filter "hs_invoice_date>=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)" \
  --properties hs_number,hs_amount_billed,hs_invoice_date

Verify the status enum the same way: hubspot properties get --type invoices --name hs_invoice_status --format json | jq -r '.options[].value'.

4. Track subscriptions

Same shape, filter on hs_subscription_status. Verify the enum values before writing the filter — do not hardcode ACTIVE/CANCELLED/PAST_DUE:

hubspot properties get --type subscriptions --name hs_subscription_status --format json \
  | jq -r '.options[].value'

# Then filter (case matters)
hubspot objects search --type subscriptions \
  --filter "hs_subscription_status=<value-from-above>" \
  --properties hs_mrr,hs_arr,hs_subscription_status

# Sum MRR across active subs
hubspot objects search --type subscriptions \
  --filter "hs_subscription_status=<active-value>" --format json \
  | jq '[.data[].properties.hs_mrr | select(. != null and . != "") | tonumber] | add // 0'

Known constraints

  • invoices, subscriptions, orders, carts need the matching read scope on the active token; 403 means the user OAuth login or private-app token is missing the scope.
  • Destructive ops (objects delete on products/quotes/line_items) often need a private-app token: export HUBSPOT_ACCESS_TOKEN=<token>. See bulk-operations/SKILL.md for the dry-run → digest → confirm flow before bulk-deleting catalog records.
  • Quote share links, PDF generation, approval routing, and from-scratch invoice creation are UI-only — the CLI updates records but cannot send a quote to a customer.
  • hs_total_discount on line items is read-only — set discount (percentage) instead.