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.

See also