JavaScript Transform
By default a subscriber receives the canonical JSON record untouched. When your server wants something else — Markdown, a different JSON shape, a plain line of text — attach a transform script to that subscriber and build the body yourself.
The contract
You provide one function:
function transform(data, context) {
// return the HTTP request BODY
}- Return a string → sent verbatim (Markdown, plain text, form-encoded, …).
- Return an object or array →
JSON.stringify(...)is sent,Content-Typestaysapplication/json. - Return anything else (
undefined, a number, a boolean) → the send fails, by design. Courier never sends empty or half-built data.
The script controls the body only. URL, method, headers and auth stay as configured on the subscriber. With a script attached, static body params are bypassed — you build the body yourself.
The sandbox
Scripts run in a sealed JavaScriptCore sandbox on-device:
- No network, no file system, no DOM, no
require, nosetTimeout. - Errors and infinite loops fail the send and the reason appears in the dispatch history.
console.log/warn/errorare captured and shown in the Test preview only.
context reference
All values are strings:
| Field | Example |
|---|---|
context.endpointName | "My Obsidian relay" |
context.url | the destination URL |
context.kind | "workout" | "sleep" | "note" | "weather" |
context.now | ISO-8601 timestamp of this dispatch |
context.timeZone | "Asia/Shanghai" |
context.locale | "zh-Hans" |
context.appVersion / context.appBuild | "1.0.0" / "1" |
context.idempotencyKey | the Idempotency-Key this send will carry |
data reference
data is the canonical record for the subscriber's kind. Dates are ISO-8601 strings (new Date(data.start) to parse); optional fields may be absent.
kind === "workout"
| Field | Type | Notes |
|---|---|---|
id | string | HealthKit UUID — the idempotency root |
activityType | string | "running", "cycling", "walking", … |
source | object | { name, bundleIdentifier?, productType?, version? } |
start, end | string | ISO-8601 |
duration | number | seconds |
distanceMeters | number? | |
activeEnergyKcal, totalEnergyKcal | number? | |
elevationGainMeters | number? | |
heartRate | object? | { averageBpm?, minBpm?, maxBpm? } |
avgPaceSecPerKm | number? | |
laps | array? | [{ index, start, duration, distanceMeters? }] |
heartRateSeries | array? | only if the HR-series toggle is on |
route | array? | only if the route toggle is on; large |
metadata | object? | HealthKit metadata pass-through |
schemaVersion | number |
kind === "sleep"
| Field | Type | Notes |
|---|---|---|
id | string | |
nightOf | string | the day this night is attributed to |
inBedStart, inBedEnd | string? | |
sleepStart, sleepEnd | string | |
timeInBedSec | number? | |
totalAsleepSec | number | |
stages | object | { awakeSec, remSec, coreSec, deepSec, unspecifiedSec } |
awakenings | number | |
efficiencyPct | number? | 0–100 |
vitals | object? | { averageHeartRateBpm?, averageHRVms?, respiratoryRate?, oxygenSaturation?, wristTemperatureDeltaC? } |
segments | array | [{ stage, start, end }], stage: inBed|awake|rem|core|deep|unspecified |
sources | array | [{ name, bundleIdentifier?, … }] |
schemaVersion | number |
kind === "note"
| Field | Type | Notes |
|---|---|---|
id | string | |
createdAt | string | ISO-8601 |
text | string | the captured thought (typed or transcribed) |
source | string | "text" | "audio" |
metadata | object? | |
schemaVersion | number |
kind === "weather"
| Field | Type | Notes |
|---|---|---|
id | string | "{sleepID}:weather" |
capturedAt | string | when measured (≈ wake time) |
weatherDate | string | the companion sleep's nightOf |
symbolName | string | SF-Symbol style, e.g. "cloud.sun" |
conditionText | string | e.g. "Cloudy" |
temperatureC, apparentTemperatureC | number | |
humidityPct | number | 0–100 |
windKmh | number | |
uvIndex | number | |
precipitationChancePct | number | 0–100 |
location | object? | { lat, lon } — only with the coordinates toggle on |
schemaVersion | number |
Examples
A custom JSON shape:
function transform(data, context) {
return {
kind: "workout",
activity: data.activityType,
km: data.distanceMeters ? +(data.distanceMeters / 1000).toFixed(2) : null,
minutes: Math.round(data.duration / 60),
avgHr: (data.heartRate && data.heartRate.averageBpm) || null,
at: data.start,
via: context.endpointName,
};
}One plain-text line:
function transform(data) {
const h = Math.floor(data.totalAsleepSec / 3600);
const m = Math.round((data.totalAsleepSec % 3600) / 60);
return "Slept " + h + "h" + m + "m on " + data.nightOf.slice(0, 10) +
" (" + data.awakenings + " awakenings)";
}A Markdown blockquote for annotations:
function transform(data) {
return "> " + data.text + "\n>\n> — " + data.createdAt;
}Workflow
- In the subscriber editor, open the script section and import a
.jsfile or paste code. The app can hand you a fully commented template to start from. - Hit Test: the script runs against a recent sample and you see the exact output body, the console logs and the endpoint's HTTP response — before anything real is sent.
- Save. From now on every dispatch to this subscriber runs through your script; script errors fail the send visibly in the dispatch history rather than delivering garbage.
Scripts are part of config backups
Unlike secrets (Keychain, never exported), transform scripts are included in configuration backups in plain text — don't hardcode credentials in them.