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, or input.values['bedrooms'] when the reference is not a plain identifier.
  • input.value(reference) — the same lookup as a function, returning null rather than undefined when there is no such question.
  • input.fields — every submitted field in order, each with fieldId, fieldKey, type, label and value. label is 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, note and submittedAt. services is an array of the types of the services you offer, such as 'residential', 'gutter' or 'conservatory'. bedrooms is a string such as '3'. address is { flatNumber, streetNumber, streetName, town, country, postcode }.
  • input.quote.distance — how far the job is from the business, as { miles, kilometres }, or null when either address cannot be located.

Answer values follow the field type:

  • number fields give a number, or null when left blank
  • money fields give whole pence, or null when 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 true or false
  • address fields give an address object, as input.quote.address above
  • 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 …". min must be more than zero and max more than min; leave max out for a single price. price/lower/upper are accepted as aliases.
  • A cost breakdown — the line items that become the invoice when the quote is accepted. Either { itemisation: [...] } alongside an optional min/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.