{ "name": "EVCS Advisor Shadow V1", "nodes": [ { "name": "Schedule Trigger", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [ 0, 0 ], "parameters": { "rule": { "interval": [ { "field": "minutes", "minutesInterval": 15 } ] } } }, { "name": "HA Snapshot Advisor", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 200, 0 ], "parameters": { "method": "POST", "url": "http://10.10.10.17:8123/api/template", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "Accept", "value": "text/plain" } ] }, "sendBody": true, "bodyParameters": { "parameters": [ { "name": "template", "value": "{{ {\n \"advisor_enabled\": states(\"input_boolean.evcs_advisor_enabled\"),\n \"advisor_agree_count\": states(\"input_number.evcs_advisor_agree_count\"),\n \"advisor_diverge_count\": states(\"input_number.evcs_advisor_diverge_count\"),\n \"revue_jour_date\": states(\"sensor.evcs_revue_jour\"),\n \"revue_jour_constats\": state_attr(\"sensor.evcs_revue_jour\", \"constats\") or [],\n \"advisor_revue_date\": states(\"sensor.evcs_advisor_revue\"),\n\n \"soc_percent\": states(\"sensor.victron_battery_soc\"),\n \"battery_current_a\": states(\"sensor.victron_battery_current\"),\n \"pv_power_w\": states(\"sensor.victron_pv_power_system\"),\n \"pv_power_10m_avg_w\": states(\"sensor.victron_pv_power_10m_avg\"),\n \"house_ac_power_w\": states(\"sensor.victron_consumption_power_l1\"),\n \"grid_state\": states(\"switch.edf_general_ok_switch\"),\n \"acin1_available\": states(\"sensor.victron_ac_in1_available\"),\n\n \"evcs_vehicle_connected\": states(\"binary_sensor.victron_evcs_vehicle_connected\"),\n \"evcs_charging\": states(\"switch.victron_evcs_charging\"),\n \"evcs_current_a\": states(\"sensor.victron_evcs_current\"),\n \"evcs_surplus_state\": states(\"input_text.victron_evcs_surplus_state\"),\n \"evcs_source_mode\": states(\"input_text.victron_evcs_source_mode\"),\n\n \"balance_status\": states(\"input_text.evcs_balance_status\"),\n \"balance_last_run\": states(\"input_text.evcs_balance_last_run\"),\n \"balance_required_soc\": states(\"input_number.evcs_balance_required_soc\"),\n \"balance_required_soc_prudent\": states(\"input_number.evcs_balance_required_soc_prudent\"),\n \"balance_energy_to_cover_kwh\": states(\"input_number.evcs_balance_energy_to_cover_kwh\"),\n \"balance_crossing_hour\": states(\"input_text.evcs_balance_crossing_hour\"),\n \"balance_grid_needed\": states(\"input_boolean.evcs_balance_grid_needed\"),\n \"balance_grid_energy_needed_kwh\": states(\"input_number.evcs_balance_grid_energy_needed_kwh\"),\n \"balance_pv_censored\": states(\"input_boolean.evcs_balance_pv_censored\"),\n \"balance_censor_reason\": states(\"input_text.evcs_balance_censor_reason\"),\n \"balance_storage_shortfall_kwh\": states(\"input_number.evcs_balance_storage_shortfall_kwh\"),\n \"balance_next_deficit_kwh\": states(\"input_number.evcs_balance_next_deficit_kwh\"),\n \"balance_next_deficit_window\": states(\"input_text.evcs_balance_next_deficit_window\"),\n \"balance_storm_level\": states(\"input_text.evcs_balance_storm_level\"),\n \"balance_storm_reserve_soc\": states(\"input_number.evcs_balance_storm_reserve_soc\"),\n\n \"modules_en_service\": states(\"input_number.evcs_balance_modules_en_service\"),\n \"bms_max_discharge_current_a\": states(\"sensor.airtria_8_us2000c_maximum_allowed_discharge_current\"),\n \"bms_max_charge_current_a\": states(\"sensor.airtria_8_us2000c_maximum_allowed_charge_current\"),\n \"grid_setpoint_w\": states(\"number.gx_device_ac_power_setpoint\"),\n \"mppt_operation_modes\": states.sensor\n | selectattr('entity_id','search','mppt')\n | selectattr('entity_id','search','operation_mode')\n | map(attribute='state') | join(','),\n \"vehicle_selected\": states(\"input_select.victron_evcs_vehicle\"),\n \"evenements_recents\": (\n [ \"EDF \" ~ states(\"switch.edf_general_ok_switch\") ~ \" depuis \" ~ relative_time(states.switch.edf_general_ok_switch.last_changed),\n \"verrou hysteresis \" ~ states(\"input_boolean.victron_grid_hysteresis_latch\") ~ \" depuis \" ~ relative_time(states.input_boolean.victron_grid_hysteresis_latch.last_changed),\n \"takeover nuit \" ~ states(\"input_boolean.victron_evcs_night_grid_takeover_needed\") ~ \" depuis \" ~ relative_time(states.input_boolean.victron_evcs_night_grid_takeover_needed.last_changed),\n \"etat EVCS \" ~ states(\"input_text.victron_evcs_surplus_state\") ~ \" depuis \" ~ relative_time(states.input_text.victron_evcs_surplus_state.last_changed),\n \"mode ACM \" ~ states(\"input_text.victron_charge_mode\") ~ \" depuis \" ~ relative_time(states.input_text.victron_charge_mode.last_changed) ]\n | join(\"|\")\n ),\n\n \"user_strategy\": states(\"input_select.victron_evcs_strategy\"),\n \"allow_grid_hc_night\": states(\"input_boolean.victron_evcs_allow_grid_hc_night\"),\n \"allow_grid_hc_midday\": states(\"input_boolean.victron_evcs_allow_grid_hc_midday\"),\n \"night_target_soc_actual\": states(\"input_number.victron_evcs_night_target_soc\"),\n \"current_max_auto_a\": states(\"input_number.victron_evcs_current_max_auto_a\"),\n \"plafond_edf_on_a\": states(\"input_number.evcs_plafond_courant_edf_on\"),\n\n \"departure_time\": states(\"input_datetime.victron_evcs_departure_time\"),\n \"departure_priority_enabled\": states(\"input_boolean.victron_evcs_departure_priority_enabled\"),\n \"required_energy_kwh\": states(\"input_number.victron_evcs_required_energy_kwh\"),\n\n \"today_remaining_kwh\": states(\"sensor.energy_production_today_remaining\"),\n \"tomorrow_kwh\": states(\"sensor.energy_production_tomorrow\")\n} | tojson }}\n" } ] }, "options": { "timeout": 15000 } }, "credentials": { "httpBearerAuth": { "id": "uv3sknYYigxBkaVN", "name": "Bearer Auth account 2" } } }, { "name": "BUILD_PROMPT", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 400, 0 ], "parameters": { "jsCode": "// =====================================================\n// EVCS ADVISOR — BUILD_PROMPT (RELECTEUR DE LA JOURNEE, 08/10/2026)\n//\n// Le conseiller n'observe plus l'instantane toutes les 15 min : 1 500 « rien a\n// signaler » contre 871 signalements sans suite, et aucun des rates du 07/10\n// (HC de midi perdue, EDF en heures pleines a 18:03, VW bloquee de 01:24 a\n// 05:14 avec EDF ferme) n'avait ete vu. Il RELIT desormais, une fois par jour,\n// les constats CALCULES par le service d'historisation (tools/revue-jour.mjs,\n// sensor.evcs_revue_jour) et propose pour chacun une piste a examiner.\n// Doctrine §79 : les chiffres viennent du code, le modele ne fait que relire.\n// Il ne pilote rien.\n//\n// Il tourne quand : le conseiller est actif, la revue du jour est publiee, il\n// y a au moins un point a examiner, et il n'a pas deja repondu aujourd'hui.\n// =====================================================\n\nconst raw = $input.first().json;\nconst input = typeof raw.data === 'string' ? JSON.parse(raw.data) : (raw.data || raw);\nconst onOff = (v) => (v ?? 'off').toString().trim().toLowerCase() === 'on';\n\nconst jourParis = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' });\nlet constats = input.revue_jour_constats;\nif (typeof constats === 'string') { try { constats = JSON.parse(constats); } catch { constats = []; } }\nif (!Array.isArray(constats)) constats = [];\nconst aExaminer = constats.filter((c) => c && c.gravite === 'a_examiner' && c.code && c.texte);\n\nconst revueDuJour = (input.revue_jour_date || '').toString().trim() === jourParis;\nconst dejaRepondu = (input.advisor_revue_date || '').toString().trim() === jourParis;\n// Pas de relance au-dela de 10 h : un modele qui echoue ne doit pas etre\n// rappele toute la journee (le courriel part a 08:15 sans lui).\nconst heureParis = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(new Date()));\n\nconst ctx = {\n advisorEnabled: onOff(input.advisor_enabled),\n jour: jourParis,\n revueDuJour,\n dejaRepondu,\n constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || ''), etat: String(c.etat || '') })),\n};\n\nconst systemPrompt = [\n \"Tu es le RELECTEUR du systeme energie d'une maison en Bretagne : 16 kWc de solaire,\",\n 'batterie de ~23 kWh utiles (0,30 kWh par point de SOC), Victron ESS SANS injection',\n '(le solaire est bride quand personne ne le prend), abonnement EDF 12 kVA, contacteur',\n 'EDF ouvert la plupart du temps, heures creuses 01:24-07:24 et 12:24-14:24, borne de',\n 'recharge pour trois vehicules. Des automates decident : le controleur de la borne,',\n \"le bilan energetique (SOC requis), l'ACM (courant de charge), l'hysteresis EDF, des\",\n 'automatisations Home Assistant.',\n '',\n 'On te donne des CONSTATS MESURES sur les dernieres 24 h. Ce sont des FAITS : ne les',\n 'conteste pas et ne les recalcule pas. Pour chacun, propose UNE piste : la cause la',\n 'plus probable et ce qu il faut verifier ou corriger. Tu ne pilotes rien.',\n '',\n 'REGLES ABSOLUES :',\n '- N ecris AUCUN chiffre qui ne figure pas dans le constat ou son contexte.',\n '- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',\n '- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',\n '- Le contexte est la raison publiee par le controleur au debut de l episode.',\n '- L etat donne les leviers a ce moment-la : consigne reseau, courant de charge max fixe',\n ' par l ACM, mode de nuit (sans_edf : la voiture ne fait pas fermer EDF ; complete : elle',\n ' charge sur EDF en HC), budget voiture sans EDF, SOC requis, cible du matin. Cherche la',\n ' cause d abord dans ces leviers (un courant ACM bas, une consigne egale a la maison, un',\n ' mode sans_edf avec budget 0 expliquent souvent le constat).',\n '',\n 'Reponds UNIQUEMENT en JSON : {\"pistes\":[{\"code\":\"\",\"piste\":\"\"}]}',\n].join('\\n');\n\nconst userPrompt = [\n `Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,\n '',\n ...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\\n constat : ${c.texte}\\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}\\n etat : ${String(c.etat || '(inconnu)').slice(0, 600)}`),\n '',\n 'JSON uniquement.',\n].join('\\n');\n\nreturn [{\n json: {\n enabled: ctx.advisorEnabled && revueDuJour && !dejaRepondu && aExaminer.length > 0 && heureParis < 10,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt },\n ],\n context: ctx,\n },\n}];\n" } }, { "name": "IF_ENABLED", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 600, 0 ], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "leftValue": "={{ $json.enabled }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ] } } }, { "name": "LLM_LOCAL_MODELS", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 800, -100 ], "parameters": { "method": "GET", "url": "http://10.10.10.8:8000/v1/models", "options": { "timeout": 5000 } }, "onError": "continueRegularOutput", "alwaysOutputData": true }, { "name": "LLM_LOCAL", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1000, -100 ], "parameters": { "method": "POST", "url": "http://10.10.10.8:8000/v1/chat/completions", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ model: $('LLM_LOCAL_MODELS').first().json.data[0].id, messages: $('BUILD_PROMPT').first().json.messages, temperature: 0.1, max_tokens: 6000 }) }}", "options": { "timeout": 120000 } }, "onError": "continueRegularOutput", "alwaysOutputData": true }, { "name": "IF_LOCAL_OK", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1200, -100 ], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "leftValue": "={{ $json.choices !== undefined && $json.choices.length > 0 }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ] } } }, { "name": "LLM_CLOUD", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1400, 40 ], "parameters": { "method": "POST", "url": "https://openrouter.ai/api/v1/chat/completions", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ model: 'anthropic/claude-haiku-4.5', messages: $('BUILD_PROMPT').first().json.messages, temperature: 0.1, max_tokens: 1000 }) }}", "options": { "timeout": 60000 } }, "credentials": { "httpHeaderAuth": { "id": "eOM9xofDHT0KMoeY", "name": "OpenRouter Advisor" } }, "onError": "continueRegularOutput", "alwaysOutputData": true }, { "name": "VALIDATE_PARSE", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1600, -30 ], "parameters": { "jsCode": "// =====================================================\n// EVCS ADVISOR — VALIDATE_PARSE (relecteur, 08/10/2026)\n// Valide STRICTEMENT la sortie LLM (locale OU cloud, format OpenAI).\n// - JSON {\"pistes\":[{code, piste}]} ; sinon REJET TOTAL.\n// - une piste dont le code n'est pas celui d'un constat est ecartee ;\n// - ANTI-INVENTION : une piste qui contient un nombre absent du constat et de\n// son contexte est ecartee (le modele ne produit aucun chiffre, §79).\n// =====================================================\n\nconst resp = $input.first().json;\nconst context = $('BUILD_PROMPT').first().json.context;\n\n// QUI A REPONDU (08/10, proprietaire : « OpenRouter, garde-le mais affiche-le\n// clairement »). Le repli cloud n'est appele que si le modele local de la Spark\n// echoue ; dans ce cas les constats ont quitte la maison et il faut le dire.\nconst viaCloud = (() => { try { return $('LLM_CLOUD').isExecuted === true; } catch { return false; } })()\n || typeof resp?.provider === 'string' || /^gen-/.test(String(resp?.id || ''));\nconst source = viaCloud ? 'openrouter' : 'local';\n\nfunction fail(status, detail) {\n return [{ json: { status, detail: (detail || '').toString().slice(0, 200), pistes: null, rejetees: 0, ecartees: [],\n source, modelUsed: (resp?.model || 'aucun').toString().slice(0, 60), context } }];\n}\n\nconst content = resp?.choices?.[0]?.message?.content;\nif (typeof content !== 'string' || !content.trim()) {\n return fail('llm_offline', resp?.error?.message || resp?.error || 'aucune réponse LLM');\n}\nconst text = content.replace(/[\\s\\S]*?<\\/think>/gi, '').replace(/```(?:json)?/gi, '').trim();\nconst start = text.indexOf('{');\nif (start === -1) return fail('invalid', 'aucun JSON dans la réponse');\nlet depth = 0, end = -1;\nfor (let i = start; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n}\nif (end === -1) return fail('invalid', 'JSON non terminé');\nlet obj;\ntry { obj = JSON.parse(text.slice(start, end + 1)); } catch (e) { return fail('invalid', `JSON invalide: ${e.message}`); }\nif (!Array.isArray(obj.pistes)) return fail('invalid', 'champ pistes absent');\n\n// Nombres d'un texte, normalises (virgule decimale -> point, sans zeros inutiles).\nconst nombres = (s) => (String(s).match(/\\d+(?:[.,]\\d+)?/g) || []).map((x) => String(Number(x.replace(',', '.'))));\n\nconst parCode = new Map(context.constats.map((c) => [c.code, c]));\n// Les pistes ECARTEES sont gardees avec leur motif : elles sont historisees\n// pour rejouer le conseiller plus tard (regression) et juger s'il progresse.\nconst pistes = [];\nconst ecartees = [];\nconst ecarter = (code, piste, motif) => ecartees.push({ code, piste: piste.slice(0, 280), motif });\nfor (const p of obj.pistes) {\n const code = (p?.code ?? '').toString().trim();\n const piste = (p?.piste ?? '').toString().trim();\n const c = parCode.get(code);\n if (!piste) { ecarter(code, '', 'piste vide'); continue; }\n if (!c) { ecarter(code, piste, 'code inconnu ou constat non a examiner'); continue; }\n const permis = new Set(nombres(`${c.texte} ${c.contexte} ${c.etat || ''}`)\n .flatMap((n) => [n, String(Math.round(Number(n))), String(Math.round(Number(n) * 10) / 10)]));\n const inventes = nombres(piste).filter((n) => !permis.has(n));\n if (inventes.length) { ecarter(code, piste, `chiffre absent du constat : ${inventes.join(', ')}`); continue; }\n if (pistes.some((x) => x.code === code)) { ecarter(code, piste, 'doublon'); continue; }\n pistes.push({ code, piste: piste.slice(0, 280) });\n}\n\nreturn [{ json: { status: 'ok', detail: '', pistes, rejetees: ecartees.length, ecartees, source,\n modelUsed: (resp.model || 'inconnu').toString().slice(0, 60), context } }];\n" } }, { "name": "BUILD_HA_CALLS", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1800, -30 ], "parameters": { "jsCode": "// =====================================================\n// EVCS ADVISOR — BUILD_HA_CALLS (relecteur, 08/10/2026)\n// GARANTIE : ce node n'ecrit QUE des entites advisor (input_text.evcs_advisor_*,\n// sensor.evcs_advisor_revue) et le logbook. AUCUN switch, AUCUNE strategie,\n// AUCUNE commande. Ne pas ajouter d'autres entites ici.\n// Les items `state_entity` sont envoyes a /api/states (capteur avec attributs),\n// les autres a /api/services//.\n// =====================================================\n\nconst { status, detail, pistes, rejetees, ecartees, source, modelUsed, context } = $input.first().json;\n\nfunction stampParis() {\n const p = new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',\n hour: '2-digit', minute: '2-digit', hour12: false }).formatToParts(new Date());\n const g = (t) => p.find((x) => x.type === t)?.value || '';\n return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;\n}\n\nconst calls = [];\nconst push = (domain, service, data) => calls.push({ json: { domain, service, data } });\nconst setText = (entity, value) => push('input_text', 'set_value', { entity_id: entity, value: (value ?? '').toString().slice(0, 250) });\n\nsetText('input_text.evcs_advisor_status', status + (detail ? `: ${detail}` : ''));\nsetText('input_text.evcs_advisor_last_run', stampParis());\nsetText('input_text.evcs_advisor_model', `${modelUsed} (${source === 'openrouter' ? 'OpenRouter, cloud' : 'local Spark'})`);\n\nif (status === 'ok' && Array.isArray(pistes)) {\n calls.push({ json: { state_entity: 'sensor.evcs_advisor_revue', data: {\n state: context.jour,\n attributes: { friendly_name: 'Revue du conseiller IA', pistes, rejetees, ecartees: ecartees || [], modele: modelUsed,\n source,\n source_texte: source === 'openrouter'\n ? 'Repli OpenRouter (cloud) : le modele local de la Spark n a pas repondu, les constats ont ete envoyes a OpenRouter.'\n : 'Modele local de la Spark : rien n est sorti de la maison.',\n constats_relus: context.constats.length,\n avertissement: 'Avis a verifier, pas des faits. Les constats chiffres sont dans sensor.evcs_revue_jour.' },\n } } });\n setText('input_text.evcs_advisor_reason',\n `Revue du ${context.jour} : ${pistes.length} piste(s)${rejetees ? `, ${rejetees} ecartee(s)` : ''}`);\n push('logbook', 'log', {\n name: 'EVCS Advisor (relecteur)',\n message: pistes.map((p) => `[${p.code}] ${p.piste}`).join(' || ') || 'aucune piste retenue',\n entity_id: 'input_text.evcs_advisor_reason',\n });\n}\n\nreturn calls;\n" } }, { "name": "Execute HA Service Advisor", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2000, -30 ], "parameters": { "method": "POST", "url": "={{ $json.state_entity ? 'http://10.10.10.17:8123/api/states/' + $json.state_entity : 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "accept", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $json.data }}", "options": { "timeout": 15000 } }, "credentials": { "httpBearerAuth": { "id": "uv3sknYYigxBkaVN", "name": "Bearer Auth account 2" } }, "onError": "continueRegularOutput" } ], "connections": { "Schedule Trigger": { "main": [ [ { "node": "HA Snapshot Advisor", "type": "main", "index": 0 } ] ] }, "HA Snapshot Advisor": { "main": [ [ { "node": "BUILD_PROMPT", "type": "main", "index": 0 } ] ] }, "BUILD_PROMPT": { "main": [ [ { "node": "IF_ENABLED", "type": "main", "index": 0 } ] ] }, "IF_ENABLED": { "main": [ [ { "node": "LLM_LOCAL_MODELS", "type": "main", "index": 0 } ], [] ] }, "LLM_LOCAL_MODELS": { "main": [ [ { "node": "LLM_LOCAL", "type": "main", "index": 0 } ] ] }, "LLM_LOCAL": { "main": [ [ { "node": "IF_LOCAL_OK", "type": "main", "index": 0 } ] ] }, "IF_LOCAL_OK": { "main": [ [ { "node": "VALIDATE_PARSE", "type": "main", "index": 0 } ], [ { "node": "LLM_CLOUD", "type": "main", "index": 0 } ] ] }, "LLM_CLOUD": { "main": [ [ { "node": "VALIDATE_PARSE", "type": "main", "index": 0 } ] ] }, "VALIDATE_PARSE": { "main": [ [ { "node": "BUILD_HA_CALLS", "type": "main", "index": 0 } ] ] }, "BUILD_HA_CALLS": { "main": [ [ { "node": "Execute HA Service Advisor", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "callerPolicy": "workflowsFromSameOwner" }, "staticData": null, "id": "tdfInZns7cqo8fU2" }