Macros let you write JavaScript that reads and writes the worksheets of the spreadsheet section they belong to. They run in a sandboxed environment with a hard 10-second timeout. The full mathjs library is available for numeric and statistical calculations.
For an overview of how to manage and run macros, see Spreadsheets.
Click Run. The target sheet’s A1 cell is set to the sum of the first ten cells of Sheet1’s column A, with a yellow background. The result row below the editor shows how many cells were written.
context — the spreadsheet API. Lets you read and write the sheets, cells, and ranges of the macro’s own spreadsheet section.
math — the full mathjs library. All numeric, statistical, matrix, and unit operations work normally. A small set of code-execution functions are disabled for safety (evaluate, import, simplify, derivative, resolve, createUnit, reviver).
A macro takes effect entirely through the writes it queues (cell.set, range.set, setStyle, …). There is no output value to return — write your results into a sheet instead.
Returns the worksheet with the given name, or — when given a number — the worksheet at that 1-based tab position. Throws if no sheet matches or the index is out of range — a typo surfaces immediately rather than silently doing nothing.
const plate = context.getSheet("Plate-1");
plate.cell("B2").set(42);
const first = context.getSheet(1); // leftmost tab, regardless of its name
Use the index form when a macro should work across templates whose sheet names vary.
Both forms resolve within the macro’s own spreadsheet section, where worksheet names are unique — there is nothing to disambiguate.
Returns the cell’s current value. Sees writes the macro has already queued — cell.set(42); cell.read() returns 42.
cell.set(value)
Queues a write. Strings starting with = are treated as formulas.
cell.setBlock(values)
Anchors a 2D array at this cell and queues writes for every element. Dimensions are inferred — handy when you don’t know how many rows a CSV will have. Returns the resulting Range.
cell.setStyle(style)
Queues a style write. style is a CSS-property map, e.g. { background: "#ffd", fontWeight: "bold" }.
cell.setMeta(meta)
Queues a per-cell metadata write — an arbitrary { key: value } payload persisted with the workbook.
A macro sees exactly the worksheets (tabs) of the spreadsheet section it belongs to. It cannot read or write other sections or other steps — use multiple worksheet tabs within one section when a macro needs to combine data from several sheets.
Reads happen against a snapshot taken when the run starts. They’re always fast and synchronous.
Writes flow through the live spreadsheet immediately, and the section auto-saves as usual.
const plate = context.getSheet("Plate-1"); // one tab
const summary = context.getSheet("Summary"); // another tab, same section
const od = plate.range("B2:B11").read().flat().filter(v => typeofv === "number");
summary.cell("A1").set("Mean OD");
summary.cell("B1").set(math.mean(od));
Interactive UI — alerts, confirms, prompts, file uploads and attachments
Attach a file to the current step — it appears in the step’s Files section. Typically you attach a file the user just picked with openFile:
await context.attachFile(
file, // an object returned by context.openFile(...)
filename?, // optional — overrides the stored file name
): { id, name } // the created file's id and name
The original bytes are uploaded as-is, regardless of which as mode you used in openFile — so you don’t need as: "blob" just to attach. The optional filename overrides the name the file is stored under.
// Let the user pick a file and attach it to this step
You can also attach content the macro generates itself, as long as the object carries the bytes in text, json, or buffer:
awaitcontext.attachFile(
{ name: "summary.txt", type: "text/plain", text: "All checks passed.\n" },
"summary.txt",
);
Attaching is only available in manually run macros, and only on steps you can edit.
Attachments persist immediately.attachFile uploads the file the moment it is called — it is not rolled back if the macro fails or is terminated later. To avoid leaving a file behind on a failed run, call attachFile near the end of your macro, after your sheet writes. If a run does fail after attaching, delete the unwanted file from the step’s Files section. Re-running a macro attaches the file again, so it can create duplicates.
The full mathjs function set is available except for these blocked functions: evaluate, simplify, derivative, resolve, import, createUnit, reviver. Calling any of them throws an error.
Fits a four-parameter logistic (4PL) curve — the standard model for ELISA and other dose-response standard curves — by unweighted least squares, using the Levenberg-Marquardt algorithm as implemented in ml-levenberg-marquardt. Not part of mathjs; a Scifeon addition.
math.fit4PL(
x, // number[] — doses/concentrations, all > 0
y, // number[] — responses, paired with x by index
options?: {
fixedLower?: number; // pin the lower asymptote to a known value (e.g. 0) and fit 3 parameters
maxIterations?: number; // solver iterations per attempt, default 200
},
): IFit4PLResult
The fitted curve, on the natural-log dose scale t = ln(x):
This is the same curve as the GraphPad/Prism form — slope equals the Hill slope, and logEC50 = -intercept / (slope * ln(10)).
Property
Meaning
lower, upper
The asymptotes (response at zero and infinite dose)
slope
Logit slope on ln(x); equals the Hill slope
intercept
Logit intercept on ln(x)
ec50
Dose at the curve midpoint, exp(-intercept/slope); NaN for a flat curve
logEC50
log10(ec50)
hillSlope
Alias of slope (GraphPad convention)
residualSS
Sum of squared residuals of the fit
iterations
Solver iterations of the winning attempt
predict(x)
The fitted response at dose x; NaN for x <= 0
Replicates are passed as repeated x values — different numbers of replicates per concentration are fine. The fit is deterministic: the same input always reproduces the same parameters.
Estimation is unweighted ordinary least squares: the parameters minimize the residual sum of squares SSE = Σ (yᵢ - y(tᵢ))² over all observations. Every replicate enters as its own observation and contributes equally — no averaging per concentration, no variance weighting.
Under the hood, fit4PL owns everything between “arrays of numbers in” and “fitted parameters out”:
Initial estimates come from a logit linearization of the data: provisional asymptotes just outside the observed response range, then a linear regression of the logit-transformed responses on log-dose. Deterministic — no random starting points.
The solver (ml-levenberg-marquardt, MIT-licensed, pinned to an exact version that only changes through Scifeon’s release process) is run once per damping magnitude (0.001, 0.1, 10). The pinned version keeps its damping fixed within a run, so multiple attempts guard against a single run stalling.
The best candidate wins: every attempt — including the untouched initial estimate — is scored by its residual sum of squares, recomputed independently of the solver, and the lowest-SSE candidate with finite parameters is returned. If none qualifies, fit4PL throws "4PL fit did not converge" rather than returning garbage.
The whole pipeline is covered by automated unit and macro-pipeline tests that run on every Scifeon platform change.
Constrained vs. free fit.fixedLower pins the lower asymptote to a known value (for example 0 for blank-corrected responses) and estimates the remaining 3 parameters; without it, all 4 are estimated. For downstream statistics: with p estimated parameters (3 or 4) and n points used, residual degrees of freedom = n - p and residual variance = residualSS / (n - p). Fitted values at any dose come from predict(x) inside the macro, or from the closed-form curve formula in sheet formulas.
fit4PL throws an Error (with a descriptive message) instead of returning a bad fit when the data cannot support one: mismatched array lengths, non-finite values, doses ≤ 0, fewer than 4 points (3 with fixedLower), fewer than 2 distinct doses, or constant responses. Filter out excluded values (empty cells, "N/A", …) before calling:
// ELISA standard curve: 8 two-fold dilutions x 4 replicates in C4:F11,
// starting concentration in B3. Empty or non-numeric cells are excluded.
Add a debugger; statement, open your browser’s developer tools (F12), then click Run. Execution pauses on that line so you can inspect variables and step through the code.
With dev tools closed, debugger does nothing and the macro runs straight through. The 10-second timeout keeps counting while you are paused, so use it for a quick look — pausing too long terminates the run.