Health sync daemon (disabled)
Disabled since February 2026. The LaunchAgent has been unloaded. The daemon was found to import corrupted multi-source sleep data from the CSV pipeline, contributing to inflated sleep debt calculations. The HealthSync iOS app is now the sole health data source with single-source filtering. Code is preserved in GitLab for reference.
This page covers the health-sync daemon that imported sleep, activity, and workout data from Apple Health. It is no longer running.
Flow Control series
- Flow Control
- Architecture
- Cursor sync daemon
- Music sync daemon
- Health sync daemon (disabled) - You are here
- HealthSync iOS app (recommended)
- Deployment
Why a separate daemon?
Apple Health does not provide an easy way to export data programmatically. I use HealthFit as a bridge to Google Sheets, then export to CSV for import.
| Challenge | Solution |
|---|---|
| No Apple Health API on Mac | HealthFit iOS app syncs to Google Sheets |
| Data scattered across formats | Google Apps Script exports to CSV daily |
| macOS CloudStorage restrictions | Apple Shortcut copies to accessible folder |
| Need incremental imports | Daemon tracks last import, processes new data |
Data flow
Prerequisites
- HealthFit app (iOS) - Syncs Apple Health to Google Sheets
- Google Drive - Stores exported CSVs
- Google Drive for Desktop (Mac) - Syncs CSVs locally
- Apple Shortcut - Copies from CloudStorage to accessible folder
Setup
1. Configure HealthFit
- Install HealthFit from the App Store
- Connect to Google Sheets in Settings
- Enable automatic sync on workout completion
This creates a Google Sheets spreadsheet with tabs for:
- Daily metrics (steps, calories, heart rate)
- Sleep data (duration, stages, efficiency)
- Weight measurements
- Workouts (type, duration, heart rate)
2. Create Google Apps Script
Add a daily export script to your HealthFit spreadsheet:
function exportHealthData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const folder = DriveApp.getFolderById('YOUR_FOLDER_ID');
const sheets = ['Metrics', 'Sleep', 'Weight', 'Workouts'];
sheets.forEach(name => {
const sheet = ss.getSheetByName(name);
if (sheet) {
// Delete existing file if present
const existing = folder.getFilesByName(name.toLowerCase() + '.csv');
while (existing.hasNext()) {
existing.next().setTrashed(true);
}
// Create new CSV
const csv = convertToCSV(sheet);
folder.createFile(name.toLowerCase() + '.csv', csv, MimeType.CSV);
}
});
}
function convertToCSV(sheet) {
const data = sheet.getDataRange().getValues();
return data.map(row =>
row.map(cell => {
if (cell instanceof Date) {
return Utilities.formatDate(cell, 'UTC', 'yyyy-MM-dd HH:mm:ss');
}
if (typeof cell === 'string' && cell.includes(',')) {
return '"' + cell.replace(/"/g, '""') + '"';
}
return cell;
}).join(',')
).join('\n');
}
Set a time-driven trigger for 5am daily.
3. Configure Google Drive sync
- Install Google Drive for Desktop
- Configure it to sync your Health export folder
- Note the local path (typically
~/Library/CloudStorage/GoogleDrive-*/My Drive/...)
4. Create Apple Shortcut
macOS restricts daemon access to ~/Library/CloudStorage/. Create a shortcut that:
- Runs a shell script to copy CSVs
- Triggers on a schedule (e.g., 6am daily)
Shell script (~/bin/health-sync.sh):
#!/bin/bash
SOURCE="$HOME/Library/CloudStorage/GoogleDrive-your.email@example.com/My Drive/HealthExports"
DEST="$HOME/health-data"
mkdir -p "$DEST/Health"
mkdir -p "$DEST/Workouts"
# Copy health CSVs
cp "$SOURCE/Health/metrics.csv" "$DEST/Health/" 2>/dev/null
cp "$SOURCE/Health/sleep.csv" "$DEST/Health/" 2>/dev/null
cp "$SOURCE/Health/weight.csv" "$DEST/Health/" 2>/dev/null
# Copy workout CSVs
cp "$SOURCE/Workouts/workouts.csv" "$DEST/Workouts/" 2>/dev/null
echo "Health data copied at $(date)"
5. Build the daemon
cd ~/Projects/tools/health-sync
npm install
npm run build
6. Configure launchd
Create ~/Library/LaunchAgents/com.example.health-sync.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.health-sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/YOUR_USER/Projects/tools/health-sync/dist/index.js</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>STATS_DASHBOARD_URL</key>
<string>https://your-stats.example.com</string>
<key>STATS_API_KEY</key>
<string>your-api-key-here</string>
<key>HEALTH_DATA_DIR</key>
<string>/Users/YOUR_USER/health-data</string>
<key>POLL_INTERVAL</key>
<string>900000</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/health-sync.log</string>
<key>StandardErrorPath</key>
<string>/tmp/health-sync.error.log</string>
</dict>
</plist>
7. Load the service
launchctl load ~/Library/LaunchAgents/com.example.health-sync.plist
Configuration
| Variable | Default | Description |
|---|---|---|
STATS_DASHBOARD_URL | Required | Dashboard base URL |
STATS_API_KEY | Required | API key for authentication |
HEALTH_DATA_DIR | ~/health-data | Path to health CSV directory |
POLL_INTERVAL | 900000 | Sync interval in ms (15 minutes) |
CSV sources
| CSV File | Local Path | Description |
|---|---|---|
sleep.csv | ~/health-data/Health/ | Sleep records from Oura/Apple Watch |
metrics.csv | ~/health-data/Health/ | Daily steps, calories, heart rate |
workouts.csv | ~/health-data/Workouts/ | Exercise sessions |
Data imported
Sleep data
| Field | Description |
|---|---|
| date | Sleep date |
| sleepStart | Bedtime (HH:MM) |
| sleepEnd | Wake time (HH:MM) |
| timeAsleepMins | Actual sleep duration |
| remMins | REM stage duration |
| deepMins | Deep stage duration |
| coreMins | Light/Core stage duration |
| efficiency | Sleep efficiency percentage |
| wakeCount | Number of wake-ups |
Daily metrics
| Field | Description |
|---|---|
| date | Metrics date |
| steps | Step count |
| activeEnergy | Active calories |
| restingHr | Resting heart rate |
| hrv | Heart rate variability |
Workouts
| Field | Description |
|---|---|
| date | Workout date |
| startTime | Start time (HH:MM) |
| type | Workout type |
| durationMins | Duration in minutes |
| calories | Calories burned |
| avgHr | Average heart rate |
Sleep debt calculation
The dashboard calculates sleep debt using a 14-day rolling window:
Sleep Debt = Sum of (Target - Actual) over 14 days
Default target is 7.5 hours (450 minutes). Positive values indicate debt, negative values indicate surplus.
AI health insights
The dashboard generates AI-powered recommendations based on:
- Last night's sleep (duration, efficiency, stages)
- Sleep debt trend (improving/worsening/stable)
- Current work session (hours, late night detection)
Insights are cached and regenerated only when new health data is imported.
Cache invalidation
When the daemon imports new sleep data, the AI insight cache is automatically cleared:
Troubleshooting
No data importing
# Check if CSVs exist
ls -la ~/health-data/Health/
ls -la ~/health-data/Workouts/
# Check daemon logs
tail -100 /tmp/health-sync.log
CloudStorage access denied
If the daemon cannot read from ~/Library/CloudStorage/, use the Apple Shortcut approach to copy files to ~/health-data/ first.
Stale data
The dashboard shows data staleness on the Health page. If data is more than 2 days old:
- Check HealthFit sync on iOS
- Verify Google Apps Script trigger ran
- Confirm Google Drive synced locally
- Run the Apple Shortcut manually
Force re-import
To re-import all historical data:
rm ~/.health-sync-state.json
launchctl unload ~/Library/LaunchAgents/com.example.health-sync.plist
launchctl load ~/Library/LaunchAgents/com.example.health-sync.plist
Next: Deployment covers Kubernetes deployment and GitOps configuration.