Skip to content

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–31required
    Sheet name without [ ] : * ? / \; names must be unique.
  • columnsarray, ≤ 1024optional
    Column widths.
  • rowsarray, ≤ 10,000optional
    Row heights.
  • dataarray of ranges, ≤ 1000optional
    Cell ranges to write.
  • formatsarray, ≤ 5000optional
    Range formats, applied in order.
  • mergesarray of ranges, ≤ 5000optional
    Ranges to merge, e.g. A1:H1.
  • freezeobjectoptional
    Freeze top rows and left columns.
JSONA minimal sheet with a formula
{
  "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. A1required
    Top-left cell of the range. Pattern ^[A-Z]{1,3}[1-9][0-9]{0,6}$.
  • values2-D arrayrequired
    Rows (1 to 100,000), each with 1 to 1024 cells, placed from at rightwards and downwards.

Ranges are written in order; a later range overwrites an earlier one where they overlap.

Cell values#

Value sentResult in the cell
nullSkip the cell: nothing is written, any existing value stays.
1250.5A number.
trueA 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.
JSONEvery kind of cell value
{
  "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"
    }
  ]
}
Coming soon

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 B2required
    Range to format.
  • boldbooleanoptional
    Bold.
  • italicbooleanoptional
    Italic.
  • underlinebooleanoptional
    Underline.
  • color#RRGGBBoptional
    Text colour.
  • fill#RRGGBBoptional
    Background colour.
  • fontstringoptional
    Font name.
  • sizenumber 4–400 (pt)optional
    Font size.
  • alignleft | center | rightoptional
    Horizontal alignment.
  • valigntop | middle | bottomoptional
    Vertical alignment.
  • wrapbooleanoptional
    Wrap text in the cell.
  • number_formatstring, 1–128optional
    Number format code in English (US) syntax: #,##0, 0.00%, DD/MM/YYYY.
  • borderfalse | border objectoptional
    Border {"width", "color"} as in text tables.
JSONHeader row, number, percent, date formats and borders
{
  "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"
      }
    }
  ]
}
Coming soon

Conditional formatting, pivot tables and charts.

Layout: columns, rows, merges, freeze#

Sheet layout

  • columns[].colcolumn letters A to ZZZrequired
    Column to size.
  • columns[].widthnumber 1–500 (mm)required
    Column width.
  • rows[].rowinteger 1–1048576required
    Row to size.
  • rows[].heightnumber 1–400 (mm)required
    Row height.
  • merges[]range, e.g. A1:H1optional
    Range merged into one cell.
  • freeze.rowsinteger 0–100optionalDefault: 0
    Number of top rows kept visible.
  • freeze.colsinteger 0–100optionalDefault: 0
    Number of left columns kept visible.
JSONMerged title, column widths, first two rows frozen
{
  "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.

JSONbang-luong.json
{
  "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"
    }
  ]
}
Build the payroll as 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());