Server-side spreadsheet automation is useful when the spreadsheet is logic, not the user interface. A Node service may need to price a quote, check a budget, score an import, or run a forecast using formulas that already exist in a workbook-shaped model.
Use @bilig/headless for that middle case: the service owns a workbook object,
changes narrow input cells, reads calculated outputs, and stores the WorkPaper
document as JSON for the next request or job.
Keep the automation boundary small:
That is a better backend contract than screen scraping a grid or duplicating spreadsheet formulas in application code.
Run this from an empty Node project:
mkdir bilig-server-automation-eval
cd bilig-server-automation-eval
npm init -y
npm pkg set type=module
npm install @bilig/headless
npm install -D tsx typescript @types/node
cat > eval.ts <<'EOF'
import {
WorkPaper,
createWorkPaperFromDocument,
exportWorkPaperDocument,
parseWorkPaperDocument,
serializeWorkPaperDocument,
} from '@bilig/headless'
type NumericCell = {
value: number
}
function numberValue(cell: unknown, label: string): number {
if (typeof cell === 'object' && cell !== null && typeof (cell as NumericCell).value === 'number') {
return (cell as NumericCell).value
}
throw new Error(`expected ${label} to be numeric, got ${JSON.stringify(cell)}`)
}
const workbook = WorkPaper.buildFromSheets({
Inputs: [
['Metric', 'Value'],
['Customers', 25],
['ARPA', 140],
['Discount', 0.05],
],
Summary: [
['Metric', 'Value'],
['Net revenue', '=Inputs!B2*Inputs!B3*(1-Inputs!B4)'],
],
})
const inputs = workbook.getSheetId('Inputs')
const summary = workbook.getSheetId('Summary')
if (inputs === undefined || summary === undefined) {
throw new Error('missing sheet')
}
const before = numberValue(workbook.getCellValue({ sheet: summary, row: 1, col: 1 }), 'before revenue')
workbook.setCellContents({ sheet: inputs, row: 1, col: 1 }, 42)
const after = numberValue(workbook.getCellValue({ sheet: summary, row: 1, col: 1 }), 'after revenue')
const saved = serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }))
const restored = createWorkPaperFromDocument(parseWorkPaperDocument(saved))
const restoredSummary = restored.getSheetId('Summary')
if (restoredSummary === undefined) {
throw new Error('missing restored Summary sheet')
}
const afterRestore = numberValue(restored.getCellValue({ sheet: restoredSummary, row: 1, col: 1 }), 'restored revenue')
console.log(
JSON.stringify(
{
before,
after,
afterRestore,
persistedBytes: saved.length,
verified: before === 3325 && after === 5586 && afterRestore === 5586,
},
null,
2,
),
)
EOF
npx tsx eval.ts
Expected output:
{
"before": 3325,
"after": 5586,
"afterRestore": 5586,
"persistedBytes": 1024,
"verified": true
}
The byte count can change between package versions. The important check is that
verified is true and the restored workbook returns the same computed value.
Good server-side fits:
| Workflow | Service input | WorkPaper output |
|---|---|---|
| Quote approval | discount, quantity, term | total price and approval status |
| Budget variance | department actuals | variance amount and review flag |
| Subscription forecast | churn, expansion, new seats | ending MRR and ARR |
| Import validation | normalized records | totals, rejected rows, and formulas |
| Agent tool call | one changed assumption | before/after values and persistence proof |
This is not an XLSX styling library and it is not full Excel automation. Use
ExcelJS or SheetJS when the main artifact is an Excel file. Use HyperFormula
when broad Excel-compatible formula coverage is the primary requirement. Use
@bilig/headless when a Node process needs a small workbook model it can edit,
verify, and save.
If this matches a backend workflow you maintain, star the repository so the next developer searching for spreadsheet automation in Node can find it: https://github.com/proompteng/bilig/stargazers.
If it almost matches but a gap blocks adoption, use the adoption blocker form: https://github.com/proompteng/bilig/discussions/new?category=general.