Quote from price list
Builds a draft quote whose every price comes from the owner's current price list. The agent does the reading and matching. The script does the arithmetic, so totals are the same every time. Anything without a price-list line stays unpriced for the owner.
Inputs
- Price list: CSV with
code,description,unit,price_cents,owner_prices.owner_pricesisyesfor lines the owner always prices by hand (large equipment, insurance work). See examples/price-list.csv. Confirm with the owner that it is the current version and note its date. - Notes: call notes, a site-visit form, field notes for extra work, or the customer's message. See examples/call-notes.md.
- Quote template and terms text, if the owner has one. Use the terms exactly as written; never edit them.
If the price list lives in a field-service or accounting system, the owner exports it, or the agent reads it through the AHL Action Gateway with the owner's agent token. The agent never holds the system login and stores no credentials.
Steps
- List the work. Turn the notes into separate work items, each with a quantity and the phrase from the notes it came from. One item per thing the customer asked for. Do not add work the notes do not mention, even if it is usually needed. Put it under questions instead ("Does moving the sink also need a drain change?").
- Match each item to a price-list code. Write the reason ("'fill thing' = fill valve, TOI-REP"). If no line fits, or two fit equally, leave
codeblank and explain innote. Never pick the nearest-sounding line to get a price. - Quantities. Use the number in the notes. If the notes give none, use 1 only for
eachorvisitunits. For hours or lengths with no number, leave the item unpriced and ask. If a quantity is an estimate (like "maybe 2.5 hours"), keep it and say so in the note. - Write
items.csv(code,qty,note), like examples/items.csv. - Run the script:
It prints the priced lines, the subtotal and the unpriced list. Its output for the example is examples/expected-output.md. Quantities may have up to 2 decimals. Each line total is rounded half up to the cent. Fix any error it reports, such as a bad quantity or an unknown code, and run it again. Never edit its numbers by hand.node scripts/price-quote.mjs examples/price-list.csv examples/items.csv - Draft the quote in the owner's template. Use the script's table as written, then add:
- Assumptions: each assumption the draft relies on ("customer supplies the faucet", "all work in one visit").
- Questions for the owner: every unpriced item and every unclear point.
- The line "Tax, discounts and final total to be set by the owner."
- For work that may grow once started (opening walls, repair work), a placeholder line for the owner's approval limit, such as "We will contact you for approval before any work beyond this quote." The FTC's consumer guidance on auto repair expects a written estimate to say so, and state law may require it. The owner decides the wording. See examples/expected-draft-quote.md.
- Save the draft as a file, or as an email draft to the owner with
gmail.draftormanual.draftthrough the gateway. In rehearsal the gateway holds every draft. A quote never goes straight to the customer: any send (gmail.send,manual.send) is always held for the owner.
Edge cases
- Price list line with price 0 or blank: treat it as owner-priced. The script refuses a price that is not a whole number of cents.
- Change order: quote only the extra work from the field notes. Name the original job and do not re-price the original lines.
- Customer asks for a discount or a match to another quote: note it for the owner. Do not apply it.
- Customer supplies parts: price the labour line only, and note who supplies the part.
- Notes include an access code, alarm code or card number: leave it out of the quote and tell the owner it was in the notes.
Never
- Invent a price, use a price from memory or a past quote, or edit a price-list line.
- Apply a discount, markup, rush fee or tax rate the price list does not contain.
- Choose equipment size, capacity or a technical method. That is the tradesperson's call.
- Send a quote to a customer or mark a job as booked or won.
- Show costs or margins if the price list holds them.
- Log in to the field-service or accounting system directly, or store any credential.
Hand over
The owner gets the draft quote, the script output and the questions. They check: each code matches the work described; estimated quantities are ones they agree with; the unpriced items are priced or removed; tax, discounts and the approval-limit line are set; the terms are the current ones. The owner sends the quote and keeps the final version, so later drafts can be compared with it.
The skill folder
Every file in the folder. Download each one into a folder with the same layout, or copy it.
quote-from-price-list/ SKILL.md examples/call-notes.md examples/expected-draft-quote.md examples/expected-output.md examples/items.csv examples/price-list.csv scripts/price-quote.mjs
SKILL.md
---
name: quote-from-price-list
description: Turns call notes, a site visit or a customer request into a draft quote priced only from the owner's price list, using a script for the arithmetic in integer cents, and lists every item it could not price, with assumptions and questions for the owner. Use when a trade or service business asks for a first-draft quote, estimate or change order from notes and its own price book; the owner checks every line and sends it.
metadata:
ahl-category: sales
ahl-jobs: quotes, sales
ahl-industries: plumbing, electrical, handyman, cleaning, landscaping, hvac
ahl-connectors: gmail, jobber, housecall-pro, quickbooks-online
ahl-workflows: sales--05-turn-call-notes-into-a-draft-quote, sales--06-build-a-small-quote-builder-tool, operations--1006-plumbing-build-a-phone-price-book-for-plumbers, operations--1044-electrical-change-order-drafts-from-field-notes
ahl-human-check: The owner checks every line, sets anything unpriced, tax and discounts, and sends the quote from their own account.
ahl-proposed-status: approved
ahl-version: 1
ahl-sources: |
FTC — Auto Repair Basics — https://consumer.ftc.gov/articles/auto-repair-basics — 2026-10-06
Google — Create and send draft emails (Gmail API) — https://developers.google.com/workspace/gmail/api/guides/drafts — 2026-10-06
---
# Quote from price list
Builds a draft quote whose every price comes from the owner's current price list. The agent does the reading and matching. The script does the arithmetic, so totals are the same every time. Anything without a price-list line stays unpriced for the owner.
## Inputs
- **Price list**: CSV with `code,description,unit,price_cents,owner_prices`. `owner_prices` is `yes` for lines the owner always prices by hand (large equipment, insurance work). See [examples/price-list.csv](examples/price-list.csv). Confirm with the owner that it is the current version and note its date.
- **Notes**: call notes, a site-visit form, field notes for extra work, or the customer's message. See [examples/call-notes.md](examples/call-notes.md).
- **Quote template** and terms text, if the owner has one. Use the terms exactly as written; never edit them.
If the price list lives in a field-service or accounting system, the owner exports it, or the agent reads it through the AHL Action Gateway with the owner's agent token. The agent never holds the system login and stores no credentials.
## Steps
1. **List the work.** Turn the notes into separate work items, each with a quantity and the phrase from the notes it came from. One item per thing the customer asked for. Do not add work the notes do not mention, even if it is usually needed. Put it under questions instead ("Does moving the sink also need a drain change?").
2. **Match each item to a price-list code.** Write the reason ("'fill thing' = fill valve, TOI-REP"). If no line fits, or two fit equally, leave `code` blank and explain in `note`. Never pick the nearest-sounding line to get a price.
3. **Quantities.** Use the number in the notes. If the notes give none, use 1 only for `each` or `visit` units. For hours or lengths with no number, leave the item unpriced and ask. If a quantity is an estimate (like "maybe 2.5 hours"), keep it and say so in the note.
4. **Write `items.csv`** (`code,qty,note`), like [examples/items.csv](examples/items.csv).
5. **Run the script**:
```bash
node scripts/price-quote.mjs examples/price-list.csv examples/items.csv
```
It prints the priced lines, the subtotal and the unpriced list. Its output for the example is [examples/expected-output.md](examples/expected-output.md). Quantities may have up to 2 decimals. Each line total is rounded half up to the cent. Fix any error it reports, such as a bad quantity or an unknown code, and run it again. Never edit its numbers by hand.
6. **Draft the quote** in the owner's template. Use the script's table as written, then add:
- **Assumptions**: each assumption the draft relies on ("customer supplies the faucet", "all work in one visit").
- **Questions for the owner**: every unpriced item and every unclear point.
- The line "Tax, discounts and final total to be set by the owner."
- For work that may grow once started (opening walls, repair work), a placeholder line for the owner's approval limit, such as "We will contact you for approval before any work beyond this quote." The FTC's consumer guidance on auto repair expects a written estimate to say so, and state law may require it. The owner decides the wording.
See [examples/expected-draft-quote.md](examples/expected-draft-quote.md).
7. **Save** the draft as a file, or as an email draft to the owner with `gmail.draft` or `manual.draft` through the gateway. In rehearsal the gateway holds every draft. A quote never goes straight to the customer: any send (`gmail.send`, `manual.send`) is always held for the owner.
## Edge cases
- **Price list line with price 0 or blank**: treat it as owner-priced. The script refuses a price that is not a whole number of cents.
- **Change order**: quote only the extra work from the field notes. Name the original job and do not re-price the original lines.
- **Customer asks for a discount or a match to another quote**: note it for the owner. Do not apply it.
- **Customer supplies parts**: price the labour line only, and note who supplies the part.
- **Notes include an access code, alarm code or card number**: leave it out of the quote and tell the owner it was in the notes.
## Never
- Invent a price, use a price from memory or a past quote, or edit a price-list line.
- Apply a discount, markup, rush fee or tax rate the price list does not contain.
- Choose equipment size, capacity or a technical method. That is the tradesperson's call.
- Send a quote to a customer or mark a job as booked or won.
- Show costs or margins if the price list holds them.
- Log in to the field-service or accounting system directly, or store any credential.
## Hand over
The owner gets the draft quote, the script output and the questions. They check: each code matches the work described; estimated quantities are ones they agree with; the unpriced items are priced or removed; tax, discounts and the approval-limit line are set; the terms are the current ones. The owner sends the quote and keeps the final version, so later drafts can be compared with it.
examples/call-notes.md
# Call notes (invented): Mrs. Delgado, Cedar Park, 2026-10-04 Upstairs toilet keeps running, needs the fill thing replaced. She bought a new kitchen faucet and wants it put in. Water heater is old (2009?), wants a price to replace, 40 gal gas. Also asked about moving the laundry sink about 12 feet, would need new supply line run, maybe 2.5 hours extra work. Wants it done one visit. Side gate code she'll text.
examples/expected-draft-quote.md
# Draft quote for the owner: Mrs. Delgado, Cedar Park (invented example)
Price list used: examples/price-list.csv (owner confirmed current, 2026-10-01). Draft prepared 2026-10-04. Not sent.
| Code | Item | Qty | Unit price | Line total | Note |
|---|---|---|---|---|---|
| CALL | Service call (first half hour included) | 1 visit | $79.00 | $79.00 | one visit for all work |
| TOI-REP | Replace toilet fill valve | 1 each | $145.00 | $145.00 | upstairs toilet keeps running |
| FAU-KIT | Install customer-supplied kitchen faucet | 1 each | $189.00 | $189.00 | faucet supplied by the customer |
| PEX-FT | PEX supply line, run | 12 foot | $4.50 | $54.00 | move laundry sink: supply line about 12 feet |
| LAB-HR | Plumber labour | 2.5 hour | $95.00 | $237.50 | move laundry sink: estimate from the notes |
Subtotal of priced lines: $704.50
Tax, discounts and final total to be set by the owner.
[OWNER: approval limit] We will contact you for approval before any work beyond this quote.
## Assumptions
- All work is done in one visit, as the customer asked ("Wants it done one visit").
- The customer supplies the kitchen faucet; the quote covers installation only.
- The laundry sink move needs about 12 feet of supply line and about 2.5 hours of labour, both as stated in the notes, not measured.
## Questions for the owner
- Water heater (WH-40, 40 gallon gas): the price list marks it owner-priced. What price, and is a permit or haul-away included?
- Moving the laundry sink will likely need drain changes too; there is no price-list line. Price it, or confirm it is not needed?
- The labour estimate of 2.5 hours for the sink move: do you agree?
Left out on purpose: the customer will text a side-gate code. It is not in the quote; keep it in the job record only.
examples/expected-output.md
| Code | Item | Qty | Unit price | Line total | Note | |---|---|---|---|---|---| | CALL | Service call (first half hour included) | 1 visit | $79.00 | $79.00 | one visit for all work | | TOI-REP | Replace toilet fill valve | 1 each | $145.00 | $145.00 | upstairs toilet keeps running | | FAU-KIT | Install customer-supplied kitchen faucet | 1 each | $189.00 | $189.00 | faucet supplied by the customer | | PEX-FT | PEX supply line, run | 12 foot | $4.50 | $54.00 | move laundry sink: supply line about 12 feet | | LAB-HR | Plumber labour | 2.5 hour | $95.00 | $237.50 | move laundry sink: estimate from the notes | Subtotal of priced lines: $704.50 Tax, discounts and the final total: set by the owner. Unpriced (2): - Replace 40 gallon gas water heater (1 each): the price list marks this line as priced by the owner - drain changes for the moved laundry sink (not on the price list): no price-list line matches; the owner prices it or says it is not offered
examples/items.csv
code,qty,note CALL,1,one visit for all work TOI-REP,1,upstairs toilet keeps running FAU-KIT,1,faucet supplied by the customer WH-40,1,40 gallon gas; existing unit about 2009 per customer PEX-FT,12,move laundry sink: supply line about 12 feet LAB-HR,2.5,move laundry sink: estimate from the notes ,1,drain changes for the moved laundry sink (not on the price list)
examples/price-list.csv
code,description,unit,price_cents,owner_prices LAB-HR,Plumber labour,hour,9500,no CALL,Service call (first half hour included),visit,7900,no TOI-REP,Replace toilet fill valve,each,14500,no FAU-KIT,Install customer-supplied kitchen faucet,each,18900,no WH-40,Replace 40 gallon gas water heater,each,0,yes PEX-FT,"PEX supply line, run",foot,450,no
scripts/price-quote.mjs
#!/usr/bin/env node
// Prices a list of quote items against the owner's price list. Local files only, no network.
// Usage: node scripts/price-quote.mjs <price-list.csv> <items.csv>
// price-list.csv: code,description,unit,price_cents,owner_prices (owner_prices: yes = the owner always prices this line)
// items.csv: code,qty,note (code blank when no price-list line matched)
// Money is integer cents. Quantities may have at most 2 decimals (e.g. 2.5 hours);
// a line total is price_cents * qty rounded half up to the cent.
import fs from "node:fs";
const QTY_DECIMALS = 2; // quantities finer than hundredths are almost always typing errors, so they are refused
function readCsv(path) {
let text;
try {
text = fs.readFileSync(path, "utf8");
} catch (e) {
console.error(`Cannot read ${path}: ${e.code}. Pass the path to a CSV file.`);
process.exit(2);
}
const rows = [];
for (const line of text.replace(/\r\n/g, "\n").split("\n")) {
if (!line.trim()) continue;
const cells = [];
let cur = "", q = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (q && c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
else if (c === '"') q = !q;
else if (c === "," && !q) { cells.push(cur); cur = ""; }
else cur += c;
}
cells.push(cur);
rows.push(cells.map((s) => s.trim()));
}
const [head, ...body] = rows;
return body.map((r) => Object.fromEntries(head.map((h, i) => [h, r[i] ?? ""])));
}
const money = (c) => `$${Math.floor(c / 100).toLocaleString("en-US")}.${String(c % 100).padStart(2, "0")}`;
const [plPath, itemsPath] = process.argv.slice(2);
if (!plPath || !itemsPath) {
console.error("Usage: node scripts/price-quote.mjs <price-list.csv> <items.csv>");
process.exit(2);
}
const priceList = new Map();
for (const p of readCsv(plPath)) {
if (!/^\d+$/.test(p.price_cents)) { console.error(`Price list line ${p.code}: price_cents "${p.price_cents}" is not a whole number of cents.`); process.exit(2); }
priceList.set(p.code, { ...p, price: Number(p.price_cents) });
}
const priced = [], unpriced = [];
let subtotal = 0;
for (const it of readCsv(itemsPath)) {
const label = it.note || it.code;
const m = new RegExp(`^\\d+(\\.\\d{1,${QTY_DECIMALS}})?$`).exec(it.qty);
if (!it.code) { unpriced.push(`${label}: no price-list line matches; the owner prices it or says it is not offered`); continue; }
const p = priceList.get(it.code);
if (!p) { unpriced.push(`${label}: code ${it.code} is not on the price list`); continue; }
if (!m || Number(it.qty) === 0) { unpriced.push(`${p.description}: quantity "${it.qty}" is missing or not a number with at most ${QTY_DECIMALS} decimals`); continue; }
if (p.owner_prices.toLowerCase() === "yes") { unpriced.push(`${p.description} (${it.qty} ${p.unit}): the price list marks this line as priced by the owner`); continue; }
const [whole, frac = ""] = it.qty.split(".");
const hundredths = Number(whole) * 100 + Number(frac.padEnd(2, "0"));
const line = Math.floor((p.price * hundredths + 50) / 100); // round half up to the cent
subtotal += line;
priced.push(`| ${p.code} | ${p.description} | ${it.qty} ${p.unit} | ${money(p.price)} | ${money(line)} | ${it.note} |`);
}
console.log("| Code | Item | Qty | Unit price | Line total | Note |");
console.log("|---|---|---|---|---|---|");
for (const l of priced) console.log(l);
console.log(`\nSubtotal of priced lines: ${money(subtotal)}`);
console.log("Tax, discounts and the final total: set by the owner.");
console.log(`\nUnpriced (${unpriced.length}):`);
for (const u of unpriced) console.log(`- ${u}`);
Use it in Claude Code
Put the folder at .claude/skills/quote-from-price-list/ in your project (or ~/.claude/skills/quote-from-price-list/ to use it in every project). Claude Code finds SKILL.md there and uses the skill when a request matches its description.
Other agents that read the Agent Skills format take the same folder.
Sources
- FTC: Auto Repair BasicsChecked 6 Oct 2026
- Google: Create and send draft emails (Gmail API)Checked 6 Oct 2026