JavaScript-Transformation
Standardmäßig erhält ein Abonnent den canonical JSON-Datensatz unverändert. Wenn dein Server etwas anderes will — Markdown, eine andere JSON-Struktur, eine schlichte Textzeile — hänge ein Transformations-Skript an diesen Abonnenten und baue den Body selbst.
Der Vertrag
Du lieferst eine Funktion:
function transform(data, context) {
// gib den HTTP-Request-BODY zurück
}- Gib einen String zurück → wird unverändert gesendet (Markdown, reiner Text, form-encoded, …).
- Gib ein Objekt oder Array zurück →
JSON.stringify(...)wird gesendet,Content-Typebleibtapplication/json. - Gib irgendetwas anderes zurück (
undefined, eine Zahl, ein Boolean) → die Sendung schlägt fehl, mit Absicht. Courier sendet nie leere oder halbfertige Daten.
Das Skript kontrolliert nur den Body. URL, Methode, Header und Auth bleiben, wie am Abonnenten konfiguriert. Mit angehängtem Skript werden statische Body-Parameter übergangen — du baust den Body selbst.
Die Sandbox
Skripte laufen in einer abgeschotteten JavaScriptCore-Sandbox auf dem Gerät:
- Kein Netzwerk, kein Dateisystem, kein DOM, kein
require, keinsetTimeout. - Fehler und Endlosschleifen lassen die Sendung fehlschlagen, und der Grund erscheint im Versandverlauf.
console.log/warn/errorwerden mitgeschnitten und nur in der Test-Vorschau angezeigt.
context-Referenz
Alle Werte sind Strings:
| Feld | Beispiel |
|---|---|
context.endpointName | "My Obsidian relay" |
context.url | die Ziel-URL |
context.kind | "workout" | "sleep" | "note" | "weather" |
context.now | ISO-8601-Zeitstempel dieses Versands |
context.timeZone | "Asia/Shanghai" |
context.locale | "zh-Hans" |
context.appVersion / context.appBuild | "1.0.0" / "1" |
context.idempotencyKey | der Idempotency-Key, den diese Sendung tragen wird |
data-Referenz
data ist der canonical Datensatz für die Art des Abonnenten. Datumsangaben sind ISO-8601-Strings (mit new Date(data.start) parsen); optionale Felder können fehlen.
kind === "workout"
| Feld | Typ | Hinweise |
|---|---|---|
id | string | HealthKit-UUID — die Wurzel der Idempotenz |
activityType | string | "running", "cycling", "walking", … |
source | object | { name, bundleIdentifier?, productType?, version? } |
start, end | string | ISO-8601 |
duration | number | Sekunden |
distanceMeters | number? | |
activeEnergyKcal, totalEnergyKcal | number? | |
elevationGainMeters | number? | |
heartRate | object? | { averageBpm?, minBpm?, maxBpm? } |
avgPaceSecPerKm | number? | |
laps | array? | [{ index, start, duration, distanceMeters? }] |
heartRateSeries | array? | nur wenn der HF-Serien-Schalter an ist |
route | array? | nur wenn der Routen-Schalter an ist; groß |
metadata | object? | HealthKit-Metadaten, unverändert durchgereicht |
schemaVersion | number |
kind === "sleep"
| Feld | Typ | Hinweise |
|---|---|---|
id | string | |
nightOf | string | der Tag, dem diese Nacht zugerechnet wird |
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"
| Feld | Typ | Hinweise |
|---|---|---|
id | string | |
createdAt | string | ISO-8601 |
text | string | der festgehaltene Gedanke (getippt oder transkribiert) |
source | string | "text" | "audio" |
metadata | object? | |
schemaVersion | number |
kind === "weather"
| Feld | Typ | Hinweise |
|---|---|---|
id | string | "{sleepID}:weather" |
capturedAt | string | Zeitpunkt der Messung (≈ Aufwachzeit) |
weatherDate | string | das nightOf des zugehörigen Schlafs |
symbolName | string | im SF-Symbol-Stil, z. B. "cloud.sun" |
conditionText | string | z. B. "Cloudy" |
temperatureC, apparentTemperatureC | number | |
humidityPct | number | 0–100 |
windKmh | number | |
uvIndex | number | |
precipitationChancePct | number | 0–100 |
location | object? | { lat, lon } — nur bei aktiviertem Koordinaten-Schalter |
schemaVersion | number |
Beispiele
Eine eigene JSON-Struktur:
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,
};
}Eine schlichte Textzeile:
function transform(data) {
const h = Math.floor(data.totalAsleepSec / 3600);
const m = Math.round((data.totalAsleepSec % 3600) / 60);
return "Geschlafen: " + h + "h" + m + "m am " + data.nightOf.slice(0, 10) +
" (" + data.awakenings + " Aufwachphasen)";
}Ein Markdown-Blockzitat für Anmerkungen:
function transform(data) {
return "> " + data.text + "\n>\n> — " + data.createdAt;
}Arbeitsablauf
- Öffne im Abonnenten-Editor den Skript-Bereich und importiere eine
.js-Datei oder füge Code ein. Die App kann dir eine vollständig kommentierte Vorlage als Ausgangspunkt geben. - Tippe auf Test: Das Skript läuft gegen einen aktuellen Beispieldatensatz und du siehst den exakten Ausgabe-Body, die Konsolen-Logs und die HTTP-Antwort des Endpunkts — bevor irgendetwas Echtes gesendet wird.
- Speichern. Ab jetzt läuft jeder Versand an diesen Abonnenten durch dein Skript; Skriptfehler lassen die Sendung sichtbar im Versandverlauf fehlschlagen, statt Müll auszuliefern.
Skripte sind Teil der Konfigurations-Backups
Anders als Geheimnisse (Schlüsselbund, werden nie exportiert) sind Transformations-Skripte im Klartext in Konfigurations-Backups enthalten — hinterlege darin keine Zugangsdaten.