Use a Spreadsheet as a Computation Model
Sometimes the right place for business logic is a spreadsheet, not a custom rules engine.
A workbook already gives you formulas, lookup tables, formatting, dropdowns, and a GUI that non-developers know how to edit. With werkbook, you can keep that workbook as an .xlsx template in your project, update a few input cells from code, recalculate, and read the computed outputs back into your application.
This works especially well when the model changes often, but the application code should stay small and stable.
When This Pattern Fits
Use a spreadsheet as the computation model when:
- operations, finance, or project managers need to adjust assumptions without a deploy
- the logic is mostly formulas, tables, rates, thresholds, and lookups
- the application only needs to provide inputs and consume outputs
- you want the model to stay inspectable in Excel, Google Sheets, or LibreOffice Calc
The important design choice is this: the spreadsheet owns the business math, and your code owns the I/O boundary.
Practical Example: Project Quote Calculator
Suppose your application creates implementation quotes for customer onboarding projects.
Sales or an internal tool provides a few inputs:
- number of integrations
- number of imported rows
- training days
- project complexity chosen from a dropdown
Operations owns the estimation logic:
- base delivery hours
- hours per integration
- hours per 100k imported rows
- hours per training day
- complexity multipliers
- PM overhead
- blended hourly rate
That is a good spreadsheet model because the logic is business-owned, easy to express in formulas, and likely to change over time.
Design the Workbook First
Create the workbook in a spreadsheet UI and save or export it as .xlsx.
One simple layout:
| Sheet | Cells | Purpose |
|---|---|---|
Inputs | B2:B5 | Values your application writes |
Rates | B2:B7, A10:B12 | Editable assumptions and lookup tables |
Quote | B2:B6 | Formula outputs your application reads |
Suggested input cells:
| Cell | Meaning |
|---|---|
Inputs!B2 | Integration count |
Inputs!B3 | Imported row count |
Inputs!B4 | Training days |
Inputs!B5 | Complexity (Low, Medium, High) |
Suggested rate cells:
| Cell | Meaning |
|---|---|
Rates!B2 | Base delivery hours |
Rates!B3 | Hours per integration |
Rates!B4 | Hours per 100k rows |
Rates!B5 | Hours per training day |
Rates!B6 | PM overhead percent |
Rates!B7 | Blended hourly rate |
Rates!A10:B12 | Complexity lookup table |
Suggested output formulas:
| Cell | Meaning | Formula |
|---|---|---|
Quote!B2 | Delivery hours | (Rates!B2 + Inputs!B2*Rates!B3 + ROUNDUP(Inputs!B3/100000,0)*Rates!B4 + Inputs!B4*Rates!B5) * XLOOKUP(Inputs!B5, Rates!A10:A12, Rates!B10:B12) |
Quote!B3 | PM hours | B2*Rates!B6 |
Quote!B4 | Total hours | SUM(B2:B3) |
Quote!B5 | Project price | B4*Rates!B7 |
Quote!B6 | Timeline in weeks | ROUNDUP(B4/120,0) |
In the spreadsheet UI, you can make the template friendlier:
- color input cells differently from formula cells
- add data validation for the complexity dropdown
- format
Quote!B5as currency - hide the
Ratessheet if you do not want casual users editing it directly
Keep the App Integration Thin
Your application does not need to rebuild the formulas. It only needs to:
- open the template workbook
- write input values into the agreed cells
- recalculate the workbook
- read the output cells
Here is a complete example in Go:
package main
import (
"fmt"
"log"
"github.com/jpoz/werkbook"
)
type ProjectInputs struct {
Integrations int
ImportedRows int
TrainingDays int
Complexity string
}
type Quote struct {
DeliveryHours float64
PMHours float64
TotalHours float64
Price float64
TimelineWeeks float64
}
func QuoteProject(templatePath string, in ProjectInputs) (Quote, error) {
wb, err := werkbook.Open(templatePath)
if err != nil {
return Quote{}, err
}
inputs := wb.Sheet("Inputs")
out := wb.Sheet("Quote")
if inputs == nil || out == nil {
return Quote{}, fmt.Errorf("template is missing Inputs or Quote sheet")
}
if err := inputs.SetValue("B2", in.Integrations); err != nil {
return Quote{}, err
}
if err := inputs.SetValue("B3", in.ImportedRows); err != nil {
return Quote{}, err
}
if err := inputs.SetValue("B4", in.TrainingDays); err != nil {
return Quote{}, err
}
if err := inputs.SetValue("B5", in.Complexity); err != nil {
return Quote{}, err
}
wb.Recalculate()
readNumber := func(cell string) (float64, error) {
v, err := out.GetValue(cell)
if err != nil {
return 0, err
}
if v.Type != werkbook.TypeNumber {
return 0, fmt.Errorf("%s returned %v", cell, v.Raw())
}
return v.Number, nil
}
deliveryHours, err := readNumber("B2")
if err != nil {
return Quote{}, err
}
pmHours, err := readNumber("B3")
if err != nil {
return Quote{}, err
}
totalHours, err := readNumber("B4")
if err != nil {
return Quote{}, err
}
price, err := readNumber("B5")
if err != nil {
return Quote{}, err
}
timelineWeeks, err := readNumber("B6")
if err != nil {
return Quote{}, err
}
return Quote{
DeliveryHours: deliveryHours,
PMHours: pmHours,
TotalHours: totalHours,
Price: price,
TimelineWeeks: timelineWeeks,
}, nil
}
func main() {
quote, err := QuoteProject("templates/project-estimator.xlsx", ProjectInputs{
Integrations: 3,
ImportedRows: 250000,
TrainingDays: 2,
Complexity: "High",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf(
"Quote: $%.0f, %.0f total hours, %.0f weeks\n",
quote.Price,
quote.TotalHours,
quote.TimelineWeeks,
)
}
The application stays simple even though the pricing model can evolve. If operations decides that High complexity should use 1.8 instead of 1.6, that is a workbook edit, not a code change.
Treat the Workbook as Executable Configuration
A good mental model is:
- the workbook is the computation model
- the input cells are the API request
- the output cells are the API response
That makes the integration much easier to maintain.
For example, you can:
- keep the
.xlsxtemplate in version control - review model changes like any other asset change
- test a few fixed input cases and assert the output cells
- save a filled-in workbook when you need an audit trail for a quote
Practical Guardrails
- Keep all application-written cells on a dedicated
Inputssheet. - Do not write over formula cells.
SetValueclears an existing formula in that cell. - Keep output cells stable and document them in one place in code.
- Use lookup tables instead of hard-coding long nested
IFchains where possible. - Validate types when reading outputs so broken formulas fail loudly.
- If the workbook is authored in Google Sheets, export it to
.xlsxbefore loading it withwerkbook.