Quote form cost calculation
A quote form cost calculation script is plain JavaScript that runs once, on the server, when a quote is submitted from that form. Write statements only — no function wrapper — and finish with a return.
Reading an answer
Every question on the form has a reference — its fieldKey, a short name like bedrooms or howManyWindows. That is how a script reads its answer:
var windows = input.values.howManyWindows || 0;
Never look an answer up by the question's wording. input.values['How many windows are there?'] is always undefined, because the wording is not a reference. It is also not stable: a business can reword a question whenever it likes and the reference stays put, so a script written against the wording would quietly start pricing everything at zero. Each question's reference is shown as Reference when you edit that question on the quote form.
What the script is given
A single input object:
input.values— the submitted answers, keyed by each question's reference (and by its field id).input.values.bedrooms, orinput.values['bedrooms']when the reference is not a plain identifier.input.value(reference)— the same lookup as a function, returningnullrather thanundefinedwhen there is no such question.input.fields— every submitted field in order, each withfieldId,fieldKey,type,labelandvalue.labelis the wording, for building a description — not for looking anything up.input.quote— the quote itself:id,quoteFormId,name,email,phoneNumber,address,postcode,postcodeLocation,services,frequency,buildingType,bedrooms,availability,noteandsubmittedAt.servicesis an array of the types of the services you offer, such as'residential','gutter'or'conservatory'.bedroomsis a string such as'3'.addressis{ flatNumber, streetNumber, streetName, town, country, postcode }.input.quote.distance— how far the job is from the business, as{ miles, kilometres }, ornullwhen either address cannot be located.
Answer values follow the field type:
- number fields give a number, or
nullwhen left blank - money fields give whole pence, or
nullwhen left blank - select and radio fields give the chosen option's
value(not its label) as a string, and checkboxes give an array of those values - switches give
trueorfalse - address fields give an address object, as
input.quote.addressabove - assets give an array of asset ids
- everything else gives a string,
''when left blank — dates as'2026-10-16', times as'14:30'
An option value that looks like a number is still a string: wrap it in Number(...) before doing arithmetic, and remember Number('5+') is NaN.
What the script must return
Every price is in pence (£12.50 is 1250). Return one of:
- A number — a single fixed price, more than zero.
return 4500; { min, max }— a price range shown to the customer as "from … to …".minmust be more than zero andmaxmore thanmin; leavemaxout for a single price.price/lower/upperare accepted as aliases.- A cost breakdown — the line items that become the invoice when the quote is accepted. Either
{ itemisation: [...] }alongside an optionalmin/max, or the array on its own. null— no price could be worked out, so the quote is left for the business to price by hand. Return{ noPrice: true, reason: 'Outside our area' }to record why.
Finishing without a return, or returning a price that is NaN or Infinity anywhere — including inside a cost breakdown — is an error, not a missing price. So is anything else not listed here.
A cost breakdown is either a flat array of items:
return { itemisation: [
{ name: 'Window clean', price: 2500 },
{ name: 'Conservatory roof', price: 1500, units: 1, description: 'Glass only' },
] };
…or an array of named categories, each with its own items:
return { itemisation: [
{ name: 'Outside', description: 'Front and back', items: [{ name: 'Windows', price: 2500 }] },
{ name: 'Inside', items: [{ name: 'Sills', price: 800 }] },
] };
Each item needs a name and a price (or unitPrice) in pence, and optionally takes units — a whole number, at least 1, default 1 — a description, and displayPrice of 'Total' or 'Individual'. Each category needs a name. A line may be negative, for a discount, but the breakdown must add up to more than zero.
A breakdown is the price in its own right: returned on its own it gives no separate estimate, and the enquirer sees the lines and their total. Return min/max alongside it only when the quote should also carry a headline price that is not the breakdown's total.
Example
For a form with a bedrooms select of '1' to '5', from a business that works within 25 miles, charges for travel beyond 10, and adds a line when the enquirer picks its gutter service:
var bedrooms = Number(input.values.bedrooms || 0);
var miles = input.quote.distance ? input.quote.distance.miles : 0;
if (miles > 25) {
return { noPrice: true, reason: 'Outside our working area' };
}
var items = [{ name: 'Standard clean', price: 3000 + (bedrooms * 800) }];
if (input.quote.services.indexOf('gutter') !== -1) {
items.push({ name: 'Gutter clean', price: 4000 });
}
if (miles > 10) {
items.push({ name: 'Travel', price: Math.round(miles * 50) });
}
return { itemisation: items };
You can write and test the script from the quote form's page in the app, under Cost calculation, or ask Sid to write it for you.