O3O DocBuilder and conversion
o3oscript for spreadsheets
Every operation of a type = sheet script: sheets, writing cell ranges, values and formulas, dates, range formats, merges, column widths, row heights and frozen panes, each with a parameter table and an example.
On this page
A type = "sheet" script builds a workbook of 1 to 50 sheets in the sheets array. Save formats: xlsx, ods, pdf. Cells are written by range: you name the top-left cell and send a two-dimensional array.
Sheets: sheet#
The sheet object
namestring, 1–31requiredSheet name without[ ] : * ? / \; names must be unique.columnsarray, ≤ 1024optionalColumn widths.rowsarray, ≤ 10,000optionalRow heights.dataarray of ranges, ≤ 1000optionalCell ranges to write.formatsarray, ≤ 5000optionalRange formats, applied in order.mergesarray of ranges, ≤ 5000optionalRanges to merge, e.g.A1:H1.freezeobjectoptionalFreeze top rows and left columns.
{
"name": "Doanh thu",
"data": [
{
"at": "A1",
"values": [
[
"Tháng",
"Doanh thu"
],
[
1,
120000000
],
[
2,
135500000
],
[
"Cộng",
{
"f": "=SUM(B2:B3)"
}
]
]
}
]
}Writing cells: data#
Each data entry
atcell, e.g. A1requiredTop-left cell of the range. Pattern^[A-Z]{1,3}[1-9][0-9]{0,6}$.values2-D arrayrequiredRows (1 to 100,000), each with 1 to 1024 cells, placed fromatrightwards and downwards.
Ranges are written in order; a later range overwrites an earlier one where they overlap.
Cell values#
| Value sent | Result in the cell |
|---|---|
null | Skip the cell: nothing is written, any existing value stays. |
1250.5 | A number. |
true | A boolean. |
"Hà Nội" | Text. Strings are ALWAYS text, even when they start with an equals sign. |
{"f": "=SUM(B2:B5)"} | A formula (up to 8192 characters, must start with =). |
{"date": "2026-09-21"} | A real date value in YYYY-MM-DD form; set the display with number_format. |
Formulas#
- English function names, A1 references, a dot as the decimal separator.
- Arguments are separated by commas or semicolons; the service turns commas outside strings into semicolons before setting the formula.
- References to another sheet use LibreOffice syntax, with a dot after the sheet name:
$'Bảng lương'.C4:C8.
{
"name": "Ví dụ",
"data": [
{
"at": "A1",
"values": [
[
"Chữ",
"Số",
"Logic",
"Ngày",
"Công thức",
"Chuỗi có dấu bằng"
]
]
},
{
"at": "A2",
"values": [
[
"Hà Nội",
1250.5,
true,
{
"date": "2026-09-21"
},
{
"f": "=B2*2"
},
"=B2*2"
]
]
},
{
"at": "E3",
"values": [
[
{
"f": "=IF(B2>1000,\"Lớn\",\"Nhỏ\")"
}
]
]
}
],
"formats": [
{
"range": "D2",
"number_format": "DD/MM/YYYY"
}
]
}Excel-style sheet references with an exclamation mark, e.g. 'Bảng lương'!C4.
Range formats: formats#
Each formats entry
rangerange, e.g. A1:H1 or B2requiredRange to format.boldbooleanoptionalBold.italicbooleanoptionalItalic.underlinebooleanoptionalUnderline.color#RRGGBBoptionalText colour.fill#RRGGBBoptionalBackground colour.fontstringoptionalFont name.sizenumber 4–400 (pt)optionalFont size.alignleft | center | rightoptionalHorizontal alignment.valigntop | middle | bottomoptionalVertical alignment.wrapbooleanoptionalWrap text in the cell.number_formatstring, 1–128optionalNumber format code in English (US) syntax:#,##0,0.00%,DD/MM/YYYY.borderfalse | border objectoptionalBorder{"width", "color"}as in text tables.
{
"name": "Định dạng",
"data": [
{
"at": "A1",
"values": [
[
"Mục",
"Số tiền",
"Tỷ lệ",
"Ngày"
],
[
"Lương",
18000000,
0.105,
{
"date": "2026-09-30"
}
]
]
}
],
"formats": [
{
"range": "A1:D1",
"bold": true,
"fill": "#167A41",
"color": "#FFFFFF",
"align": "center",
"valign": "middle",
"wrap": true
},
{
"range": "B2:B100",
"number_format": "#,##0"
},
{
"range": "C2:C100",
"number_format": "0.00%"
},
{
"range": "D2:D100",
"number_format": "DD/MM/YYYY",
"align": "center"
},
{
"range": "A1:D2",
"border": {
"width": 0.5,
"color": "#7F7F7F"
}
}
]
}Conditional formatting, pivot tables and charts.
Layout: columns, rows, merges, freeze#
Sheet layout
columns[].colcolumn letters A to ZZZrequiredColumn to size.columns[].widthnumber 1–500 (mm)requiredColumn width.rows[].rowinteger 1–1048576requiredRow to size.rows[].heightnumber 1–400 (mm)requiredRow height.merges[]range, e.g. A1:H1optionalRange merged into one cell.freeze.rowsinteger 0–100optionalDefault:0Number of top rows kept visible.freeze.colsinteger 0–100optionalDefault:0Number of left columns kept visible.
{
"name": "Danh sách",
"columns": [
{
"col": "A",
"width": 12
},
{
"col": "B",
"width": 60
},
{
"col": "C",
"width": 30
}
],
"rows": [
{
"row": 1,
"height": 10
}
],
"data": [
{
"at": "A1",
"values": [
[
"DANH SÁCH NHÂN SỰ"
],
[
"STT",
"Họ và tên",
"Phòng ban"
],
[
1,
"Nguyễn Văn An",
"Kỹ thuật"
]
]
}
],
"merges": [
"A1:C1"
],
"formats": [
{
"range": "A1",
"bold": true,
"size": 14,
"align": "center"
}
],
"freeze": {
"rows": 2,
"cols": 1
}
}Complete example: payroll#
bang-luong.json builds a two-sheet payroll: rounding formulas, totals, MAX, COUNTIF, cross-sheet SUMIF, a date cell, number formats, merges and frozen panes. The script has 34 units.
{
"o3oscript": 1,
"type": "sheet",
"meta": {
"title": "Bảng lương tháng 9 năm 2026",
"author": "Phòng Hành chính Nhân sự",
"lang": "vi-VN"
},
"sheets": [
{
"name": "Bảng lương",
"columns": [
{
"col": "A",
"width": 12
},
{
"col": "B",
"width": 48
},
{
"col": "C",
"width": 32
},
{
"col": "D",
"width": 30
},
{
"col": "E",
"width": 22
},
{
"col": "F",
"width": 30
},
{
"col": "G",
"width": 30
},
{
"col": "H",
"width": 32
}
],
"rows": [
{
"row": 1,
"height": 10
},
{
"row": 3,
"height": 9
}
],
"data": [
{
"at": "A1",
"values": [
[
"BẢNG LƯƠNG THÁNG 9 NĂM 2026"
]
]
},
{
"at": "A2",
"values": [
[
"Ngày lập:",
{
"date": "2026-09-30"
}
]
]
},
{
"at": "A3",
"values": [
[
"STT",
"Họ và tên",
"Phòng ban",
"Lương cơ bản",
"Ngày công",
"Phụ cấp",
"Bảo hiểm (10,5%)",
"Thực lĩnh"
]
]
},
{
"at": "A4",
"values": [
[
1,
"Nguyễn Văn An",
"Kỹ thuật",
18000000,
22,
1500000,
{
"f": "=ROUND(D4*0.105,0)"
},
{
"f": "=ROUND(D4/22*E4,0)+F4-G4"
}
],
[
2,
"Trần Thị Bình",
"Kinh doanh",
15000000,
21,
2000000,
{
"f": "=ROUND(D5*0.105,0)"
},
{
"f": "=ROUND(D5/22*E5,0)+F5-G5"
}
],
[
3,
"Lê Hoàng Cường",
"Kỹ thuật",
22000000,
22,
1500000,
{
"f": "=ROUND(D6*0.105,0)"
},
{
"f": "=ROUND(D6/22*E6,0)+F6-G6"
}
],
[
4,
"Phạm Thu Dung",
"Kế toán",
14000000,
20,
800000,
{
"f": "=ROUND(D7*0.105,0)"
},
{
"f": "=ROUND(D7/22*E7,0)+F7-G7"
}
],
[
5,
"Đỗ Minh Đức",
"Hỗ trợ khách hàng",
12000000,
22,
800000,
{
"f": "=ROUND(D8*0.105,0)"
},
{
"f": "=ROUND(D8/22*E8,0)+F8-G8"
}
]
]
},
{
"at": "A9",
"values": [
[
"Tổng cộng",
null,
null,
{
"f": "=SUM(D4:D8)"
},
{
"f": "=SUM(E4:E8)"
},
{
"f": "=SUM(F4:F8)"
},
{
"f": "=SUM(G4:G8)"
},
{
"f": "=SUM(H4:H8)"
}
]
]
},
{
"at": "A11",
"values": [
[
"Lương thực lĩnh cao nhất:",
null,
null,
{
"f": "=MAX(H4:H8)"
}
]
]
},
{
"at": "A12",
"values": [
[
"Số người đủ 22 ngày công:",
null,
null,
{
"f": "=COUNTIF(E4:E8,22)"
}
]
]
}
],
"merges": [
"A1:H1",
"A9:C9",
"A11:C11",
"A12:C12"
],
"formats": [
{
"range": "A1:H1",
"bold": true,
"size": 14,
"align": "center",
"valign": "middle",
"color": "#167A41"
},
{
"range": "B2",
"number_format": "DD/MM/YYYY",
"align": "left"
},
{
"range": "A3:H3",
"bold": true,
"fill": "#167A41",
"color": "#FFFFFF",
"align": "center",
"valign": "middle",
"wrap": true
},
{
"range": "A4:A8",
"align": "center"
},
{
"range": "D4:D9",
"number_format": "#,##0"
},
{
"range": "E4:E9",
"number_format": "0",
"align": "center"
},
{
"range": "F4:H9",
"number_format": "#,##0"
},
{
"range": "A9:H9",
"bold": true,
"fill": "#E8F3EC"
},
{
"range": "A3:H9",
"border": {
"width": 0.5,
"color": "#7F7F7F"
}
},
{
"range": "D11",
"number_format": "#,##0",
"bold": true
},
{
"range": "D12",
"number_format": "0",
"bold": true
}
],
"freeze": {
"rows": 3,
"cols": 2
}
},
{
"name": "Theo phòng ban",
"columns": [
{
"col": "A",
"width": 40
},
{
"col": "B",
"width": 22
},
{
"col": "C",
"width": 34
}
],
"data": [
{
"at": "A1",
"values": [
[
"Phòng ban",
"Số người",
"Tổng thực lĩnh"
]
]
},
{
"at": "A2",
"values": [
[
"Kỹ thuật",
{
"f": "=COUNTIF($'Bảng lương'.C4:C8,A2)"
},
{
"f": "=SUMIF($'Bảng lương'.C4:C8,A2,$'Bảng lương'.H4:H8)"
}
],
[
"Kinh doanh",
{
"f": "=COUNTIF($'Bảng lương'.C4:C8,A3)"
},
{
"f": "=SUMIF($'Bảng lương'.C4:C8,A3,$'Bảng lương'.H4:H8)"
}
],
[
"Kế toán",
{
"f": "=COUNTIF($'Bảng lương'.C4:C8,A4)"
},
{
"f": "=SUMIF($'Bảng lương'.C4:C8,A4,$'Bảng lương'.H4:H8)"
}
],
[
"Hỗ trợ khách hàng",
{
"f": "=COUNTIF($'Bảng lương'.C4:C8,A5)"
},
{
"f": "=SUMIF($'Bảng lương'.C4:C8,A5,$'Bảng lương'.H4:H8)"
}
]
]
}
],
"formats": [
{
"range": "A1:C1",
"bold": true,
"fill": "#167A41",
"color": "#FFFFFF",
"align": "center"
},
{
"range": "C2:C5",
"number_format": "#,##0"
},
{
"range": "A1:C5",
"border": {
"width": 0.5,
"color": "#7F7F7F"
}
}
]
}
],
"save": [
{
"format": "xlsx",
"filename": "bang-luong.xlsx"
}
]
}curl -sS http://localhost:8080/v1/build \
-H "Authorization: Bearer O3O_DEMO_KEY" \
-H "Content-Type: application/json" \
--data-binary @bang-luong.json \
-o bang-luong.xlsx -w "HTTP %{http_code}\n"import { readFile, writeFile } from "node:fs/promises";
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };
const script = JSON.parse(await readFile("bang-luong.json", "utf8"));
const res = await fetch(`${BASE_URL}/v1/build`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify(script),
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("bang-luong.xlsx", Buffer.from(await res.arrayBuffer()));import json
import requests
BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}
with open("bang-luong.json", encoding="utf-8") as f:
script = json.load(f)
r = requests.post(f"{BASE_URL}/v1/build", headers=HEADERS, json=script, timeout=90)
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bang-luong.xlsx", "wb") as fh:
fh.write(r.content)<?php
$payload = file_get_contents("bang-luong.json");
$ch = curl_init("http://localhost:8080/v1/build");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
file_put_contents("bang-luong.xlsx", $body);using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
var json = await File.ReadAllTextAsync("bang-luong.json");
using var res = await http.PostAsync("http://localhost:8080/v1/build",
new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("bang-luong.xlsx", await res.Content.ReadAsByteArrayAsync());