All Posts
Guide

Use a Spreadsheet as a Computation Model

Keep business logic in an editable workbook, then write inputs and read calculated outputs with werkbook.

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:

SheetCellsPurpose
InputsB2:B5Values your application writes
RatesB2:B7, A10:B12Editable assumptions and lookup tables
QuoteB2:B6Formula outputs your application reads

Suggested input cells:

CellMeaning
Inputs!B2Integration count
Inputs!B3Imported row count
Inputs!B4Training days
Inputs!B5Complexity (Low, Medium, High)

Suggested rate cells:

CellMeaning
Rates!B2Base delivery hours
Rates!B3Hours per integration
Rates!B4Hours per 100k rows
Rates!B5Hours per training day
Rates!B6PM overhead percent
Rates!B7Blended hourly rate
Rates!A10:B12Complexity lookup table

Suggested output formulas:

CellMeaningFormula
Quote!B2Delivery 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!B3PM hoursB2*Rates!B6
Quote!B4Total hoursSUM(B2:B3)
Quote!B5Project priceB4*Rates!B7
Quote!B6Timeline in weeksROUNDUP(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!B5 as currency
  • hide the Rates sheet 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:

  1. open the template workbook
  2. write input values into the agreed cells
  3. recalculate the workbook
  4. 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 .xlsx template 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 Inputs sheet.
  • Do not write over formula cells. SetValue clears 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 IF chains where possible.
  • Validate types when reading outputs so broken formulas fail loudly.
  • If the workbook is authored in Google Sheets, export it to .xlsx before loading it with werkbook.