{
  "version": "1.0.0",
  "exportedAt": "2026-06-09T06:09:25.268Z",
  "workflow": {
    "name": "HealthKit Wearable to Epic Observation",
    "description": "Patient mobile app pushes wearable / Apple HealthKit readings; the platform authenticates to Epic (backend), maps the reading to a FHIR R4 Observation via a Liquid data-formatter, and writes it to the Epic sandbox."
  },
  "flow": {
    "tab": {
      "id": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
      "type": "tab",
      "label": "HealthKit Wearable to Epic Observation",
      "disabled": false,
      "info": "Patient mobile app pushes wearable / Apple HealthKit readings; the platform authenticates to Epic (backend), maps the reading to a FHIR R4 Observation via a Liquid data-formatter, and writes it to the Epic sandbox.",
      "env": []
    },
    "nodes": [
      {
        "id": "d4f771db5da7e35f",
        "type": "group",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "name": "Wearable / HealthKit -> Epic Observation",
        "style": {
          "stroke": "#7234AE",
          "fill": "#efe6f7",
          "label": true,
          "color": "#7234AE"
        },
        "nodes": [
          "fc35724e8a04dedf",
          "f6a63c166b189519",
          "ae9910c42a3157fc",
          "6238d7a7c51026e2",
          "df9a3603640d128f",
          "e1f2a3b4c5d6e7f8",
          "1a2b3c4d5e6f7a8b",
          "9c8d7e6f5a4b3c2d",
          "c7cc4cd3076f3fe3",
          "71c86f23342354c5",
          "80108df93e18bd74",
          "126e393b0126efc0",
          "0f53db8b4c679d6a",
          "3ffd0c1e9487254a",
          "6481e85d0b260dbc",
          "2f0ec842ab41ec0b"
        ],
        "x": 34,
        "y": 59,
        "w": 2692,
        "h": 282
      },
      {
        "id": "fc35724e8a04dedf",
        "type": "inject",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Simulate Apple Watch Heart Rate",
        "props": [
          {
            "p": "payload"
          }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "{\"patient_id\":\"YOUR-PATIENT-ID-HERE\",\"device\":\"Apple Watch Series 9\",\"demo\":true,\"samples\":[{\"type\":\"HKQuantityTypeIdentifierHeartRate\",\"value\":72,\"unit\":\"count/min\"}]}",
        "payloadType": "json",
        "x": 220,
        "y": 100,
        "wires": [
          [
            "6238d7a7c51026e2"
          ]
        ]
      },
      {
        "id": "f6a63c166b189519",
        "type": "http in",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "HealthKit Webhook (Live)",
        "url": "/healthkit/observation",
        "method": "post",
        "upload": false,
        "swaggerDoc": "",
        "x": 250,
        "y": 180,
        "wires": [
          [
            "6238d7a7c51026e2",
            "ae9910c42a3157fc"
          ]
        ]
      },
      {
        "id": "ae9910c42a3157fc",
        "type": "http response",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "200 OK (ack to app)",
        "statusCode": "200",
        "headers": {},
        "x": 500,
        "y": 220,
        "wires": []
      },
      {
        "id": "6238d7a7c51026e2",
        "type": "function",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Normalize HealthKit Sample",
        "func": "// The mobile app posts raw HealthKit/wearable samples.\n// Demo fallback so an empty Inject / probe still produces a reading.\nlet body = msg.payload;\nif (!body || !Array.isArray(body.samples) || body.samples.length === 0) {\n    body = {\n        patient_id: \"YOUR-PATIENT-ID-HERE\",\n        device: \"Apple Watch Series 9\",\n        demo: true,\n        samples: [{ type: \"HKQuantityTypeIdentifierHeartRate\", value: 72, unit: \"count/min\" }]\n    };\n}\n\n// HealthKit quantity type -> Epic flowsheet row id (fsid) + LOINC (for text) + UCUM.\n// Epic files Observations to FLOWSHEET ROWS. A bare LOINC can map to multiple rows\n// (\"Multiple Flowsheet rows found\"), so we send the Epic flowsheet row id instead.\n// fsid \"8\" = Heart Rate is verified for this Epic org (returned in the ambiguity error).\n// Other rows are placeholders - replace with your organization's flowsheet row ids before using them.\nconst HK_MAP = {\n    HKQuantityTypeIdentifierHeartRate:        { fsid: \"8\",  loinc: \"8867-4\",  display: \"Heart rate\",        unit: \"beats/minute\", ucum: \"/min\", category: \"vital-signs\" },\n    HKQuantityTypeIdentifierOxygenSaturation: { fsid: \"10\", loinc: \"59408-5\", display: \"Oxygen saturation\", unit: \"%\",            ucum: \"%\",    category: \"vital-signs\" },\n    HKQuantityTypeIdentifierBodyTemperature:  { fsid: \"6\",  loinc: \"8310-5\",  display: \"Body temperature\",  unit: \"Cel\",          ucum: \"Cel\",  category: \"vital-signs\" },\n    HKQuantityTypeIdentifierBodyMass:         { fsid: \"9\",  loinc: \"29463-7\", display: \"Body weight\",       unit: \"kg\",           ucum: \"kg\",   category: \"vital-signs\" },\n    HKQuantityTypeIdentifierStepCount:        { fsid: \"\",   loinc: \"55423-8\", display: \"Number of steps\",   unit: \"steps\",        ucum: \"1\",    category: \"activity\" }\n};\n\n// Take the most recent sample\nconst sample = body.samples.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0];\nconst map = HK_MAP[sample.type] || HK_MAP.HKQuantityTypeIdentifierHeartRate;\n\n// Demo mode (Inject): jitter the value + stamp 'now' so each run files a NEW reading.\n// Epic rejects an identical value at the same instant (\"Reading already exists\").\n// Real app payloads carry the device's true timestamp - dedup is then desirable.\nconst isDemo = body.demo === true;\nconst value = isDemo ? Math.max(40, sample.value + Math.floor(Math.random() * 9) - 4) : sample.value;\nconst effective = isDemo ? new Date().toISOString() : (sample.timestamp || new Date().toISOString());\n\n// Flat shape consumed by the Liquid data-formatter\nmsg.payload = {\n    patient_id: body.patient_id || \"YOUR-PATIENT-ID-HERE\",\n    flowsheet_id: map.fsid,\n    loinc_code: map.loinc,\n    display: map.display,\n    value: value,\n    unit: map.unit,\n    ucum: map.ucum,\n    category: map.category,\n    effective: effective,\n    device: body.device || \"Wearable\"\n};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 520,
        "y": 140,
        "wires": [
          [
            "df9a3603640d128f"
          ]
        ]
      },
      {
        "id": "df9a3603640d128f",
        "type": "ec-auth-backend",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Epic Auth (B2B)",
        "integrationId": "",
        "integrationMetadata": {
          "ehr": "epic",
          "type": "backend",
          "scopes": "patient/*.* user/*.* launch profile fhirUser openid system/*.write system/Observation.read system/Observation.write system/Patient.read system/ExplanationOfBenefit.read system/DocumentReference.read system/Appointment.read system/Encounter.read system/Condition.read system/MedicationRequest.read system/MedicationStatement.read system/AllergyIntolerance.read"
        },
        "customScopes": "",
        "outputFormat": "token_only",
        "outputField": "accessToken",
        "outputFieldType": "msg",
        "x": 740,
        "y": 140,
        "wires": [
          [
            "e1f2a3b4c5d6e7f8"
          ]
        ]
      },
      {
        "id": "e1f2a3b4c5d6e7f8",
        "type": "function",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Prep Encounter Search",
        "func": "// Epic requires an encounter reference on the Observation.\n// Find the patient's most recent encounter. ec-fhir-search reads\n// the token + filter from msg.payload, so keep the normalized fields\n// and just add what the search needs (extra keys are ignored downstream).\nmsg.payload.access_token = msg.accessToken;\nmsg.payload.searchFilter = {\n    patient: msg.payload.patient_id,\n    _count: 1,\n    _sort: \"-date\"\n};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 950,
        "y": 140,
        "wires": [
          [
            "1a2b3c4d5e6f7a8b"
          ]
        ]
      },
      {
        "id": "1a2b3c4d5e6f7a8b",
        "type": "ec-fhir-search",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Find Latest Encounter",
        "integrationId": "",
        "integrationMetadata": {
          "ehr": "epic",
          "type": "backend"
        },
        "resourceType": "Encounter",
        "searchFilter": "",
        "filters": "[]",
        "count": "",
        "sort": "",
        "include": "",
        "outputField": "lastEncounter",
        "outputFieldType": "msg",
        "x": 1180,
        "y": 140,
        "wires": [
          [
            "9c8d7e6f5a4b3c2d"
          ]
        ]
      },
      {
        "id": "9c8d7e6f5a4b3c2d",
        "type": "function",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Add Encounter Ref",
        "func": "// Pull the encounter id out of the search Bundle and add it to the\n// normalized payload so the Liquid template can reference it.\nconst bundle = msg.lastEncounter || (msg.ch && msg.ch.fhir && msg.ch.fhir.search) || {};\nconst entries = bundle.entry || [];\nconst encId = entries.length && entries[0].resource ? entries[0].resource.id : null;\n\nif (!encId) {\n    node.error(\"No Encounter found for patient \" + msg.payload.patient_id + \" - cannot file Observation\", msg);\n    return null;\n}\n\nmsg.payload.encounter_id = encId;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1390,
        "y": 140,
        "wires": [
          [
            "c7cc4cd3076f3fe3"
          ]
        ]
      },
      {
        "id": "c7cc4cd3076f3fe3",
        "type": "ec-hl7-mapper",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Wearable Data -> FHIR Observation",
        "templateType": "custom",
        "template": "{\n  \"resourceType\": \"Observation\",\n  \"status\": \"final\",\n  \"category\": [\n    { \"coding\": [ { \"system\": \"http://terminology.hl7.org/CodeSystem/observation-category\", \"code\": \"{{ category | default: 'vital-signs' }}\" } ] }\n  ],\n  \"code\": {\n    \"coding\": [ { \"system\": \"urn:oid:YOUR-EPIC-FLOWSHEET-OID\", \"code\": \"{{ flowsheet_id }}\", \"display\": \"{{ display }}\" } ],\n    \"text\": \"{{ display }} (LOINC {{ loinc_code }})\"\n  },\n  \"subject\": { \"reference\": \"Patient/{{ patient_id }}\" },\n  \"encounter\": { \"reference\": \"Encounter/{{ encounter_id }}\" },\n  \"effectiveDateTime\": \"{{ effective }}\",\n  \"valueQuantity\": {\n    \"value\": {{ value }},\n    \"unit\": \"{{ ucum }}\",\n    \"system\": \"http://unitsofmeasure.org\",\n    \"code\": \"{{ ucum }}\"\n  },\n  \"device\": { \"display\": \"{{ device }}\" }\n}",
        "outputFormat": "json",
        "inputField": "payload",
        "inputFieldType": "msg",
        "outputField": "",
        "outputFieldType": "msg",
        "x": 1650,
        "y": 140,
        "wires": [
          [
            "71c86f23342354c5"
          ]
        ]
      },
      {
        "id": "71c86f23342354c5",
        "type": "function",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Wrap for FHIR Create",
        "func": "// Mapper put the rendered Observation on msg.payload.\n// ec-fhir-create needs: msg.payload.access_token + msg.payload.resource\nlet resource = msg.payload;\nif (typeof resource === 'string') {\n    resource = JSON.parse(resource);\n}\n// Mapper may return a wrapper; unwrap if needed\nif (resource && resource.payload && !resource.resourceType) {\n    resource = resource.payload;\n}\n\nmsg.payload = {\n    access_token: msg.accessToken,\n    resource: resource\n};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1920,
        "y": 140,
        "wires": [
          [
            "80108df93e18bd74"
          ]
        ]
      },
      {
        "id": "80108df93e18bd74",
        "type": "ec-fhir-create",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Create Observation in Epic",
        "integrationId": "",
        "integrationMetadata": {
          "ehr": "epic",
          "type": "backend",
          "scopes": "patient/*.* user/*.* launch profile fhirUser openid system/*.write system/Observation.read system/Observation.write system/Patient.read system/ExplanationOfBenefit.read system/DocumentReference.read system/Appointment.read system/Encounter.read system/Condition.read system/MedicationRequest.read system/MedicationStatement.read system/AllergyIntolerance.read"
        },
        "resourceType": "Observation",
        "resource": "",
        "outputField": "",
        "outputFieldType": "msg",
        "x": 2160,
        "y": 140,
        "wires": [
          [
            "126e393b0126efc0"
          ]
        ]
      },
      {
        "id": "126e393b0126efc0",
        "type": "function",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Format Confirmation",
        "func": "// ec-fhir-create returns the created resource (server-assigned id) on msg.payload,\n// and metadata on msg.fhir / msg.ch.fhir.create.\nconst created = (msg.ch && msg.ch.fhir && msg.ch.fhir.create) || msg.payload || {};\nconst obsId = (msg.fhir && msg.fhir.resourceId) || created.id || \"created\";\nconst vq = created.valueQuantity || {};\nconst codeText = (created.code && created.code.text) || \"Observation\";\n\nmsg.confirmation = {\n    \"EHR STATUS\": \"LIVE EPIC CONNECTION\",\n    \"PATIENT\": (created.subject && created.subject.reference) || \"Patient\",\n    \"ENCOUNTER\": (created.encounter && created.encounter.reference) || \"n/a\",\n    \"READING\": codeText + \": \" + (vq.value !== undefined ? vq.value + \" \" + (vq.unit || \"\") : \"n/a\"),\n    \"SOURCE\": (created.device && created.device.display) || \"Wearable\",\n    \"EHR RECORD\": \"Observation/\" + obsId,\n    \"TIMESTAMP\": new Date().toISOString()\n};\nmsg.payload = msg.confirmation;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 2400,
        "y": 140,
        "wires": [
          [
            "0f53db8b4c679d6a",
            "3ffd0c1e9487254a"
          ]
        ]
      },
      {
        "id": "0f53db8b4c679d6a",
        "type": "debug",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Confirmation",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 2600,
        "y": 140,
        "wires": []
      },
      {
        "id": "3ffd0c1e9487254a",
        "type": "http request",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "d": true,
        "g": "d4f771db5da7e35f",
        "name": "Confirm back to mobile app (disabled)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "https://your-app.example.com/api/wearable/confirm",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 2540,
        "y": 260,
        "wires": [
          []
        ]
      },
      {
        "id": "6481e85d0b260dbc",
        "type": "catch",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Catch FHIR/Auth errors",
        "scope": null,
        "uncaught": false,
        "x": 480,
        "y": 300,
        "wires": [
          [
            "2f0ec842ab41ec0b"
          ]
        ]
      },
      {
        "id": "2f0ec842ab41ec0b",
        "type": "debug",
        "z": "workflow_0578bb42-3752-4c16-98ec-46509225f31e",
        "g": "d4f771db5da7e35f",
        "name": "Error",
        "active": true,
        "tosidebar": true,
        "console": true,
        "tostatus": true,
        "complete": "error",
        "targetType": "msg",
        "statusVal": "error",
        "statusType": "auto",
        "x": 720,
        "y": 300,
        "wires": []
      }
    ]
  },
  "requirements": [
    {
      "ehr": "epic",
      "type": "backend",
      "scopes": "patient/*.* user/*.* launch profile fhirUser openid system/*.write system/Observation.read system/Observation.write system/Patient.read system/ExplanationOfBenefit.read system/DocumentReference.read system/Appointment.read system/Encounter.read system/Condition.read system/MedicationRequest.read system/MedicationStatement.read system/AllergyIntolerance.read"
    },
    {
      "ehr": "epic",
      "type": "backend"
    }
  ]
}