openJIIDocs
Devices & Protocols

Writing macros

Read measurements and workbook context, then return analysis-ready values.

A macro is a data-processing script: it turns a measurement or earlier workbook values into a JSON object of named results. openJII stores Python, JavaScript, and R macros. On the web, the backend runs each language in its own isolated Lambda sandbox. The mobile field flow runs JavaScript locally and Python in Pyodide; it does not currently dispatch R macros.

Open Macros and select New macro to choose a language, write the code, and optionally associate compatible protocols. You can also create or select a macro while adding a Macro cell to a workbook. Put the macro after the values it needs, then run the workbook with representative data.

Create a macro (opens in a new tab)

Know the macro inputs

NameWhat the macro receives
jsonThe triggering measurement: normally the nearest preceding output, scoped to the device whose macro is running. A non-empty array is unwrapped to its first object by the web wrappers.
ctxCompleted upstream questions, protocols, commands, and macros keyed by canonical workbook name. Names are lowercased, punctuation becomes underscores, and duplicate names resolve to the nearest upstream value.
Return valueA JSON-serializable object: a Python dictionary, JavaScript object, or R list. Its keys become the macro output available to later cells and data processing.

Question values use an { answer: ... } object. Named protocol, command, and macro entries contain their output object. Only upstream cells are included; the macro cannot read itself or a later cell. The host passes ctx itself to macro code; there is no ctx.byId API.

Start with the triggering measurement in Python

The variable is named json, but in Python it is the measurement dictionary, not the standard-library module:

readings = json["readings"]
mean_reading = sum(readings) / len(readings)

return {
    "mean_reading": mean_reading,
    "reading_count": len(readings),
}

Use keys that are stable in the selected protocol's output. Return a dictionary even for one value so downstream cells and exported tables have named fields.

Read earlier workbook values with ctx

Name the producing cells Baseline and Stress. Their canonical keys are baseline and stress:

baseline = ctx["baseline"]["value"]
stress = ctx["stress"]["value"]
device = ctx.get("$device", {})

return {
    "change_from_baseline": stress - baseline,
    "device_id": device.get("id"),
}

For a question named Treatment group, read ctx["treatment_group"]["answer"]. Names beginning with a number gain a question_ prefix; choose simple, unique cell names rather than depending on that fallback.

In multi-device runs, each macro invocation receives the upstream results for that same device. ctx["$device"] can contain family, id, name, firmwareVersion, batteryPercent, and zero-based index when the host knows them. Keep the device ID in the output when it is useful for auditing, but do not use the connection index as a permanent device identifier.

Run from context only

The web workbook runner can execute a macro with no preceding measurement when at least one upstream ctx value exists. In that case, json is an empty dictionary. An answer-driven Python macro can therefore be as small as:

treatment = ctx["treatment_group"]["answer"]

return {
    "treatment": treatment,
    "is_control": treatment == "control",
}

This ctx-only execution is a web workbook capability. Test the full mobile flow separately before relying on it during field work.

JavaScript equivalent

JavaScript receives the same json and ctx contract:

const baseline = ctx.baseline.value;
const current = json.value;

return {
  change_from_baseline: current - baseline,
  device_id: ctx.$device?.id ?? null,
};

R macros receive json, ctx, and an output environment in the web sandbox; return an R list. Use the web workbook runner to test R macros because the mobile runner currently selects only JavaScript or Python.

Runtimes and available libraries

Where a macro runs decides which libraries you can rely on. The web sandboxes ship scientific libraries and a shared helper library; the mobile Python runtime is deliberately minimal.

RuntimeLanguageLibraries available
Web sandbox (backend Lambda)Python 3.12numpy (np), pandas (pd), scipy, plus the openJII helper library
Web sandbox (backend Lambda)JavaScript (Node)The openJII helper library (no npm packages; no Date, require, or eval)
Web sandbox (backend Lambda)RBase R and jsonlite, plus the openJII helper library
Mobile app (Pyodide)PythonPython standard library only: no numpy/pandas/scipy and no helper library

Write mobile-run Python macros against the standard library only. If a macro needs numpy, pandas, scipy, or the helper functions below, keep it on the web workbook runner.

Helper utilities

In every web runtime (Python, JavaScript, R) openJII injects a shared helper library, callable directly by name. It mirrors the PhotosynQ macro helpers so existing analysis code ports over. The main families:

  • Arrays: ArrayNth, ArrayRange, ArrayZip, ArrayUnZip.
  • Statistics: MathMEAN, MathMEDIAN, MathMIN, MathMAX, MathSUM, MathSTDEV, MathSTDEVS, MathSTDERR, MathVARIANCE, MathROUND, MathLN, MathLOG.
  • Regressions: MathLINREG, MathPOLYREG, MathMULTREG, MathEXPINVREG.
  • Protocol lookup: GetIndexByLabel, GetLabelLookup, GetProtocolByLabel to pull values out of a measurement by its protocol label.
  • Domain: TransformTrace, calcSunAngle.
  • Messages: info, warning, danger to attach notes to a result.
readings = json["readings"]

return {
    "mean": MathMEAN(readings),
    "stdev": MathSTDEV(readings),
    "fit": MathLINREG(ArrayRange(1, len(readings) + 1), readings),
}

These helpers are not available in the mobile Pyodide runtime; use plain Python there.

Treat ctx as read-only

The host constructs ctx from shared upstream state, so never assign into it. Enforcement differs by runtime:

  • Web Python recursively converts dictionaries to MappingProxyType and lists to tuples.
  • Web JavaScript deep-clones and recursively freezes ctx.
  • Web R locks the ctx binding; R's copy-on-modify behavior isolates nested changes for each item.
  • Mobile JavaScript structured-clones and recursively freezes ctx.
  • Mobile Pyodide currently receives a JSON-deserialized Python dictionary. It is not frozen, so the read-only contract depends on macro discipline there.

The triggering json measurement is separate from ctx. Do not mutate either input; create and return a new result object.

Test before publishing

  1. Use a saved measurement with the same keys and nesting the protocol will produce.
  2. Test missing, empty, and boundary values explicitly.
  3. Add the macro after its producers in a workbook and check every canonical ctx name.
  4. For multi-device work, confirm each result uses the matching device's measurement and identity.
  5. Confirm the returned object contains only JSON-serializable values.
  6. Run on both web and mobile if the workbook will be used in both places.

For the measurement recipe that feeds a macro, continue with Writing protocols. For the security and infrastructure rationale, see ADR: Execute macros in isolated language sandboxes.

On this page