Build on Sortia

@sortia/engine 0.3.0sortia-mcp 0.3.0spec v1

Sortia gives spreadsheet decisions their odds inside Google Sheets: estimates in, odds out, plus optimization, decision trees, forecasting, statistics and machine learning. The engine behind the odds, the optimizer and the statistics also runs headless in plain Node, driven by a small JSON document, so your scripts and your AI agents can use it too.

Where to get it: the packages on this page are not on npm yet. Today the source lives in the Sortia repository. Want early access? Email hello@sortia.io.

The engine: @sortia/engine

@sortia/engine (version 0.3.0) is a plain Node library with zero runtime dependencies. It needs Node 18 or newer. It is not a rewrite of the add-on: the package loads the exact engine source files the Sheets add-on ships and wraps them with a thin spec layer. The numbers you get in Node are the numbers the add-on produces, by construction, and the whole thing is covered by the same automated test suite.

The public API is small:

The model spec

One versioned JSON document describes a model. specVersion is always 1 today, and kind selects the tool family, one of 18: simulation, optimization, riskOptimization, decisionTree, goalSeek, dataTable, statTest, fit, regression and forecast, plus the project-planning and decision-tools kinds weighted, mcda, game, cpm, ccpm, evm, riskreg and scurve.

Everything is referenced by declared names, never by spreadsheet cell addresses. Formulas are ordinary spreadsheet expressions over those names, the same functions you would type into a cell. Unknown fields, unknown parameters, and undeclared names are rejected with precise errors instead of being silently ignored, which makes the format friendly to code generation: a wrong spec fails loudly with a pointer to the exact field.

A worked example: startup runway

How many months of runway does a startup have when burn, growth, and starting revenue are all estimates? This is a complete, runnable spec:

{
  "specVersion": 1,
  "kind": "simulation",
  "name": "Startup runway",
  "trials": 20000,
  "seed": 42,
  "sampling": "lhs",
  "inputs": [
    { "name": "monthly_burn",   "dist": "triangular",
      "params": { "min": 70000, "mode": 90000, "max": 120000 } },
    { "name": "revenue_growth", "dist": "triangular",
      "params": { "min": 0.03, "mode": 0.12, "max": 0.20 } },
    { "name": "starting_mrr",   "dist": "pert",
      "params": { "min": 15000, "mode": 22000, "max": 30000 } }
  ],
  "correlations": [
    { "a": "monthly_burn", "b": "starting_mrr", "rho": 0.35 }
  ],
  "model": [
    { "name": "starting_cash",    "formula": "1800000" },
    { "name": "revenue_12mo",     "formula":
      "starting_mrr * ((1 + revenue_growth)^12 - 1) / revenue_growth" },
    { "name": "cash_at_month_12", "formula":
      "starting_cash - 12 * monthly_burn + revenue_12mo" },
    { "name": "runway_months",    "formula":
      "IF(monthly_burn <= starting_mrr, 60, starting_cash / (monthly_burn - starting_mrr))" }
  ],
  "outputs": ["runway_months", "cash_at_month_12"]
}

Run it:

const { run } = require('@sortia/engine');
const result = run(spec);

const runway = result.outputs[0];
runway.stats.mean;      // 25.7516 months
runway.stats.median;    // 25.4867
runway.percentiles;     // p5 20.4849, p95 32.1179, full ladder p1 to p99
runway.stats.minimum;   // 17.9854
runway.stats.maximum;   // 40.0267
runway.sensitivity;     // tornado, by |Spearman|: monthly_burn -0.961,
                        //   starting_mrr -0.097, revenue_growth -0.001

result.outputs[1].stats.mean;  // cash_at_month_12: 1216196.59

Those numbers are not illustrative. They are what version 0.1.0 returns for this spec, every time, on every machine. Each output also carries the raw per-run values, and the result echoes a reproducibility receipt: spec version, kind, engine version, and seed.

The determinism contract

run(spec) is a pure function of the spec document. That means:

What a seed guarantees, and what it does not

Guaranteed: same inputs, same seed, same engine build, same answer, bit for bit. Not close, identical, on any machine, in any browser, at any later date. Nothing on a run path reads the clock or calls Math.random, and every termination budget is an iteration count rather than a stretch of time, so a slow machine and a fast one do the same work and return the same digits.

Not guaranteed: the same answer across engine builds. A release that improves how a number is computed changes that number. It happened on 2026-09-02, when the generator moved to sfc32 and seeded results stopped matching the old generator's, and it will happen again whenever we find an answer that was wrong. We would rather change a number than hold a known-wrong one steady, and we would rather say so than let you find out.

That is what the receipt under every result is for. It names the spec version, the engine version, the seed, the run count, the sampler and two hashes. Two runs whose receipts agree must agree to the last digit; two runs whose receipts name different builds are two different runs, and the receipt says which before anyone has to argue about it. Every release that moved a number is dated in the changelog. The contract in full is in the repository, at docs/REPRODUCIBILITY.md.

SIPmath libraries

Where a distribution leaves or enters Sortia it travels as a SIPmath 3.0 library, the open standard from probabilitymanagement.org for passing uncertain quantities between tools as one small JSON file. In the add-on, Risk Analysis, the Monte Carlo panel, Project schedule risk and Distribution Fitting save their results as a .SIPmath library, validated against the Standard's published schema; a Risk run saves every output, or the model's full input set with its correlation matrix. Risk Analysis reads a library back as inputs: Metalog 1.0 entries on an HDR generator are used, and GeneralizedMetalog and Metalog 2.0 entries, stored arrays and lookup tables are listed but skipped with a reason. A coherent run option draws each SIPmath input from the file's own seeds, so its runs are the ones any other SIPmath tool reads from the same file; an input the file correlates through a copula, or one you truncate or correlate in Sortia, shares the shape rather than the run order. Because the file is a published standard, your own scripts can read it with nothing from Sortia installed.

MCP quickstart

sortia-mcp (version 0.3.0) wraps the engine as a local Model Context Protocol server over stdio. Everything runs in-process on your machine; by default the server makes no network calls. Until it reaches npm, run it from the repository:

cd sortia/packages/mcp
npm install

Then point your MCP client at it. For clients configured with JSON:

{
  "mcpServers": {
    "sortia": {
      "command": "node",
      "args": ["/absolute/path/to/sortia/packages/mcp/bin/sortia-mcp.js"]
    }
  }
}

For Claude Code:

claude mcp add sortia -- node /absolute/path/to/sortia/packages/mcp/bin/sortia-mcp.js

The server exposes one tool per engine, 21 in all as of 2026-10-12. The ones most agents reach for first:

Validation errors pass through to the agent verbatim, with stable codes, JSON Pointer paths, and hints, so a model that emits a bad spec can read the error and fix its own output.

Every tool's description opens with when to use it, before what it does, so an agent choosing among the 21 can read one sentence. Every one of those 21 carries the MCP annotations readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: false: all of them are pure local compute. run_simulation, optimize and run_risk_optimization take detail: "concise" for a short result (a reading, the headline statistics, three percentile rungs and the top drivers) when the full ladder would only cost tokens; the default, "full", is unchanged. The package README shows three worked multi-tool sequences (fit a distribution and simulate with it; simulate, then optimize the decision under that uncertainty; sweep a data table and goal-seek each row), each run by the test suite as printed.

Sharing is off by default. If the host starts the server with SORTIA_MCP_PUBLISH=1, two more tools appear: publish_result (preview the exact page first; nothing is sent until the call is repeated with confirm: true, and then one request goes to sortia.io/api/sharePublish) and revoke_link (take down a link this server published). Those two carry readOnlyHint: false and openWorldHint: true, which is how an agent can tell them apart from the local 21. A published link expires on its own at the expiry the service returns, and revoking it makes the page answer 410.

Pointing an assistant at Sortia without the server? llms.txt is the plain-text summary of the product and this page, written for a model to read.

Without leaving the sheet

Two surfaces sit between the packages above and an ordinary spreadsheet, and neither one needs this page's toolchain. Live estimate cells put an odds formula in the grid itself, so the answer moves the moment an estimate changes and there is no run button. Connecting a BI tool is the other direction: Power BI, Tableau and Looker Studio read the report tab Sortia writes, with no export step.

Questions

Building something on Sortia, or want the packages before they land on npm? Email hello@sortia.io.

Estimates in, odds out.

Home· Start here· Templates· Proof· Open a model· Vision· Pricing· Teams· Tell someone· Privacy· Terms· Developers· Support· Changelog· Validation· Performance· © 2026 Sortia · Made for Google Sheets™. Google Sheets™ and Google Workspace™ are trademarks of Google LLC.