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.
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:
validate(spec)checks a spec without running it. It reports every problem at once, each with a stable machine code (E_PARAM_UNKNOWN,E_CIRCULAR, and so on), a JSON Pointer path to the offending field, and usually a hint with the fix.run(spec)validates, runs, and returns a typed result for the spec's kind.describeCapabilities()returns the machine-readable feature list: every distribution family and its parameters, the formula function whitelist, optimization engines, limits, and the determinism contract.- The spec's JSON Schema (draft-07) ships in the package as
spec.schema.json, so external validators and CI can check specs without running the engine.
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:
- The same spec with the same seed produces bit-identical results. Not close, identical.
- The seed lives in the spec for every stochastic kind (simulation, riskOptimization, and optimization with the evolutionary engine). There is no run-time seed override and no unseeded random mode.
- Nothing on the run path reads the clock or calls
Math.random. All termination budgets are iteration or generation counts, never wall-clock time. - Kinds that consume no randomness (decision trees, goal seek, data tables, statistical tests, fitting, regression, forecasting, and lp or nonlinear optimization) are bit-identical with no seed at all.
- The order of the
inputsarray is part of the contract: reordering inputs changes the random stream, so it changes the results. - Version numbers take numeric output seriously: any change that alters the numbers for an existing spec and seed is a major version bump, even when the change is a bug fix. To reproduce results exactly, pin the spec, the seed, and the engine major version.
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:
run_simulation: estimates in, odds out. A seeded run over named formulas with 36 distribution families, optional rank correlations, percentile ladders, and tornado sensitivity.optimize: constrained optimization with three engines (simplex, Nelder-Mead, differential evolution) and a per-constraint satisfaction report.evaluate_decision_tree: expected-value rollback with the optimal policy and the full risk profile.goal_seek: solve one constant so a formula hits a target value.fit_distribution: fit candidate distributions to data, ranked by Kolmogorov-Smirnov, each with a fragment ready to paste intorun_simulation.run_stat_test: t, z, F, ANOVA, chi-square, rank-based tests, and two-proportion A/B tests.describe_capabilities: the engine's full accepted vocabulary. Have your agent call this first before authoring complex specs.
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.