Extensions
An extension adds commands, worksheet functions and script libraries to Methodox Office for everyone who installs it, rather than for one document. It is written in the same languages as a document's own scripts, Lua, C# or Divooka graphs, and runs under the same rules.
What an extension can add
| What | Where it appears |
|---|---|
| Menu commands | Rows of Tools ▸ Extensions in the applications the extension is for (Impulse, Formula, Expo and Nu), each run on the window's document as one step of Edit ▸ Undo |
| Worksheet functions | =PREFIX.NAME(...) in every Formula workbook, listed in Insert Function; the prefix is the extension's, such as TOOLS. or FX., and never replaces a name a workbook already has |
| Script libraries | Loaded by name from any script: require("rates") in Lua, #load "rates" in C# |
That is all an extension can do. It cannot handle document events, add menus of its own, toolbar buttons or keys, or change what opening, saving or recalculating does, so installing one never changes how a document behaves until somebody runs what it added. Language packs will become a fourth kind of extension later.
What its code may do
Exactly what a document's own code may (see the programming model): a command edits the document it is run on, in one transaction that is rolled back if it fails; a function computes from its arguments and changes nothing, under the same time limit as a document's LUA. functions; a library runs inside the script that loads it. The edition decides what graphs may use, as it does for documents.
An extension reaches the network only through the host (host.getJson(...) in Lua, Host.GetJson(...) in C#), and only at the websites its manifest names. You see them before you install it, installing grants them, and they are granted to that version's exact package: an update with different code is a different package. File access and full trust are not available to extensions.
Installing and enabling
On the Start Centre's Extensions page: Install from File…, or drop a .dvb on the Start Centre, or install from a catalogue added with Add Catalogue…. Each extension's card has a check box for each application it is made for; untick one to disable it there. Uninstall removes it.
Extensions are installed for you alone, in %AppData%\DivookaOffice\extensions, so no administrator rights are needed, and one person's extensions are not another's.
Packages are not signed yet. Anything that did not come with the suite is shown as not verified; a catalogue's index carries each package's SHA-256, and a download that does not match it is deleted rather than installed.
Office Tools
Office Tools is the official extension that comes with Methodox Office. It is installed and enabled for each person the first time an application starts, and can be disabled but not uninstalled.
| Where | What it does | |
|---|---|---|
| Summary Sheet | Formula ▸ Tools ▸ Extensions | Groups the first table (or the block at A1) by its first column, sums the numeric columns onto a Summary sheet, and puts a live SUM under each |
| Format All Tables | Impulse ▸ Tools ▸ Extensions | Thin grey borders, a shaded bold header row and a header repeated on every page, for every table |
TOOLS.SLUG, TOOLS.WORDCOUNT, TOOLS.INITIALS, TOOLS.SPELLNUMBER |
Formula cells | =TOOLS.SPELLNUMBER(1234.5) is "One thousand two hundred thirty-four and 50/100" |
tools |
Lua and C# scripts | local tools = require("tools"); in C#, #load "tools" then Tools.Slug(...) |
From the command line
mos manages extensions without the Start Centre, and builds them:
| Command | What it does |
|---|---|
mos extension pack <folder> [-o <file.dvb>] |
Packs an extension's folder into a package, after checking its manifest and that every script it names is there; prints the package's SHA-256 |
mos extension index <folder> [--title <name>] |
Writes index.json for a folder of packages, which makes the folder a catalogue |
mos extension install <file.dvb> |
Installs a package for you |
mos extension install <catalogue> <id> |
Downloads and installs an extension from a catalogue's address |
mos extension list |
What is installed, and where it is enabled |
mos extension enable <id> [<app>...], disable |
Enables or disables it in the applications named, or in every one it is for |
mos extension uninstall <id> |
Removes one you installed |
The package
An extension is a Divooka app bundle, a .dvb file: a zip with divooka.json at its root and the scripts under OfficeExtensions/, by convention one folder per application and Shared/ for libraries. It is the same package format Divooka uses for its own libraries.
ExchangeRates/
divooka.json
README.md
OfficeExtensions/
Formula/InsertRates.lua a command
Formula/functions.lua the FX. functions
Impulse/RatesTable.csx a command, in C#
Shared/rates.lua the "rates" library
divooka.json declares everything; a script it does not name never runs.
{
"format": "divooka-app-bundle/1",
"kind": "library",
"title": "Exchange Rates",
"package": {
"name": "exchange-rates",
"version": "1.0.0",
"description": "Today's exchange rates in Formula and Impulse: a Rates sheet, a rates table, and functions that format an amount in a currency.",
"authors": [ "Example Publisher" ],
"license": "MIT",
"readme": "README.md",
"requires": { "methodox-office": ">=0.4.0" }
},
"office": {
"id": "example/exchange-rates",
"publisher": "Example Publisher",
"apps": [ "Formula", "Impulse" ],
"commands": [
{ "id": "insert-rates", "title": "Insert Exchange Rates", "app": "Formula", "script": "OfficeExtensions/Formula/InsertRates.lua", "description": "Writes today's rates against the base currency onto a Rates sheet." },
{ "id": "rates-table", "title": "Insert Rates Table", "app": "Impulse", "script": "OfficeExtensions/Impulse/RatesTable.csx", "description": "Adds a table of today's rates for the major currencies at the end of the document." }
],
"functions": [ { "prefix": "FX", "script": "OfficeExtensions/Formula/functions.lua" } ],
"libraries": [ { "name": "rates", "script": "OfficeExtensions/Shared/rates.lua" } ],
"capabilities": [ "network.get https://api.frankfurter.dev" ]
}
}
| Field | What it is |
|---|---|
kind |
Always library for an extension |
package.name, package.version |
The package's name and its version, as major.minor.patch |
package.requires |
methodox-office with the versions of Methodox Office it runs in, such as >=0.4.0; checked when it is installed and when it loads |
office.id |
owner/name, unique to the extension |
office.publisher |
Who made it, shown on its card |
office.apps |
The applications it is for: Impulse, Formula, Expo, Nu |
office.commands[] |
Each command's id, its title on the menu, the app whose Tools ▸ Extensions it goes in, its script, and a description shown as its tooltip |
office.functions[] |
A prefix and a function script, whose functions with names in capitals become PREFIX.NAME |
office.libraries[] |
A library's name and its script; one name may have a Lua and a C# script |
office.capabilities[] |
The websites its code may fetch from, each network.get https://<origin> |
A function script is written as a workbook's own worksheet functions are: every top-level function whose name is in capitals is a function, and a --- comment above it is its description.
Building one: the Exchange Rates sample
Exchange Rates uses every extension point there is: a Lua command in Formula, a C# command in Impulse, two functions and a library, and one website. Make the folder above with the manifest, then the four scripts.
The library, OfficeExtensions/Shared/rates.lua, fetches through the host, so it works only at the website the manifest names:
local rates = {}
local symbols = { USD = "$", EUR = "€", GBP = "£", JPY = "¥", CNY = "¥", KRW = "₩", INR = "₹", CAD = "C$", AUD = "A$", CHF = "CHF " }
--- Today's rates against a base currency: { date = "2026-10-30", base = "USD", rates = { EUR = 0.89, ... } }.
function rates.latest(base)
base = (base or "USD"):upper()
return host.getJson(host.url("https://api.frankfurter.dev/v1/latest?base={0}", base))
end
--- The currency's symbol, or its code and a space when it has none here.
function rates.symbol(code)
code = tostring(code or ""):upper()
return symbols[code] or (code .. " ")
end
--- An amount written in a currency, rounded to its usual places: 1234.5 in EUR is "€1,234.50".
function rates.format(amount, code)
local places = (code == "JPY" or code == "KRW") and 0 or 2
local text = string.format("%." .. places .. "f", math.abs(amount))
local whole, fraction = text:match("^(%d+)(%.?%d*)$")
whole = whole:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
return (amount < 0 and "-" or "") .. rates.symbol(code) .. whole .. fraction
end
return rates
The functions, OfficeExtensions/Formula/functions.lua, become =FX.FORMAT(1234.5, "EUR") and =FX.SYMBOL("GBP"). They format what the sheet gives them and fetch nothing:
local rates = require("rates")
--- An amount written in a currency: =FX.FORMAT(1234.5, "EUR") is "€1,234.50".
---@param amount number
---@param code string
function FORMAT(amount, code)
return rates.format(amount, code:upper())
end
--- A currency's symbol: =FX.SYMBOL("GBP") is "£".
---@param code string
function SYMBOL(code)
return (rates.symbol(code):gsub(" $", ""))
end
The Formula command, OfficeExtensions/Formula/InsertRates.lua, writes today's rates onto a Rates sheet, against the document's Base parameter when it has one:
local rates = require("rates")
-- Reading a parameter the document does not have is an error, so it is read under pcall.
local has, base = pcall(function() return param.Base end)
if not has or base == nil or base == "" then base = "USD" end
local today = rates.latest(base)
local codes = {}
for code in pairs(today.rates) do codes[#codes + 1] = code end
table.sort(codes)
local rows = {}
for _, code in ipairs(codes) do rows[#rows + 1] = { code, today.rates[code] } end
local sheet = doc.sheets:find("Rates")
if sheet then sheet.usedRange:clear() else sheet = doc.sheets:add("Rates") end
local written = sheet:range("A1"):write(rows, { header = { "Currency", "Per " .. today.base } })
written:resize(1, 2).format.bold = true
sheet:cells(2, 2):resize(#rows, 1).format.numberFormat = "0.0000"
sheet:cells(#rows + 3, 1).value = "Rates of " .. today.date .. " from frankfurter.dev"
print(string.format("Wrote %d rates against %s for %s.", #rows, today.base, today.date))
The Impulse command, OfficeExtensions/Impulse/RatesTable.csx, is C#, where Doc is the Impulse document and Host fetches:
string[] wanted = { "EUR", "GBP", "JPY", "CNY", "CAD", "AUD" };
JsonNode today = Host.GetJson("https://api.frankfurter.dev/v1/latest?base=USD")!;
object?[,] cells = new object?[wanted.Length + 1, 2];
cells[0, 0] = "Currency";
cells[0, 1] = "Per US dollar";
for (int i = 0; i < wanted.Length; i++)
{
cells[i + 1, 0] = wanted[i];
cells[i + 1, 1] = today["rates"]![wanted[i]]!.GetValue<double>().ToString("0.0000", System.Globalization.CultureInfo.InvariantCulture);
}
Doc.Paragraphs.Add("Exchange rates on " + (string?)today["date"]);
var table = Doc.AddTable(wanted.Length + 1, 2, cells);
table.SetBorders();
table.BoldRow(1);
Print("Added rates for " + wanted.Length + " currencies.");
Then pack it, install it and try it:
mos extension pack ExchangeRates -o exchange-rates-1.0.0.dvb
mos extension install exchange-rates-1.0.0.dvb
mos extension list
Packing the same files twice writes the same bytes. Open a workbook and type =FX.FORMAT(1234.5, "EUR"), or choose Tools ▸ Extensions ▸ Insert Exchange Rates. Write a script in the Scripts pane while you work on an extension's code, and pack it when it does what you want.
Offering it from a catalogue
Put packages in a folder and write the index beside them:
mos extension index <folder> --title "My extensions"
Serve the folder from any web server or share it, and add its address in the Start Centre with Add Catalogue…; mos extension install <address> example/exchange-rates installs from it at a prompt.
Not there yet
Signed packages, language packs, extension commands in Math, functions that fetch (a function that fetches answers #N/A), and extensions with file access or full trust.