{
  "name": "Pipeline B - Onboarding to v2 Agent",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "pipeline-b",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "webhook-trigger-b",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const fs = require('fs');\nconst path = require('path');\n\nconst filePath = $input.item.json.body.transcript_path;\nconst accountId = $input.item.json.body.account_id;\nconst onboardingText = fs.readFileSync(filePath, 'utf8');\n\nif (!accountId) throw new Error('account_id is required in the request body');\n\nconst v1MemoPath = path.join('/outputs', 'accounts', accountId, 'v1', 'account_memo.json');\nlet v1Memo;\ntry {\n  v1Memo = JSON.parse(fs.readFileSync(v1MemoPath, 'utf8'));\n} catch(e) {\n  throw new Error(`v1 memo not found for account ${accountId}. Run Pipeline A first. Path: ${v1MemoPath}`);\n}\n\nreturn { json: { account_id: accountId, v1_memo: v1Memo, onboarding_text: onboardingText } };"
      },
      "id": "read-and-load",
      "name": "Read File & Load v1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const https = require('https');\n\nconst apiKey = $env.GEMINI_API_KEY;\nconst { v1_memo, onboarding_text } = $input.item.json;\n\nconst prompt = `You are an expert data extraction assistant for Clara Answers.\n\nYou are processing an ONBOARDING call transcript. The client already did a demo call, and we have a preliminary (v1) account memo. Extract NEW or UPDATED information from the onboarding call.\n\nCRITICAL RULES:\n1. ONLY extract information EXPLICITLY stated in this onboarding transcript.\n2. NEVER invent or hallucinate details.\n3. If a field is discussed and confirmed, include it. If not discussed, set to null.\n4. Onboarding data OVERRIDES demo data when there is a conflict.\n\nExisting v1 Account Memo:\n${JSON.stringify(v1_memo, null, 2)}\n\nReturn ONLY valid JSON with the same structure, containing ONLY the fields that are NEW or CHANGED. Fields not discussed should be null (preserved from v1).\n\nAlso include 'onboarding_changes_summary' listing what changed and why.\n\n{\n  \"account_id\": \"same as v1\",\n  \"company_name\": \"string or null if unchanged\",\n  \"business_hours\": { \"days\": \"string or null\", \"start\": \"string or null\", \"end\": \"string or null\", \"timezone\": \"string or null\" },\n  \"office_address\": \"string or null\",\n  \"services_supported\": [],\n  \"emergency_definition\": [],\n  \"emergency_routing_rules\": { \"who_to_call\": [], \"order\": \"string or null\", \"fallback\": \"string or null\" },\n  \"non_emergency_routing_rules\": \"string or null\",\n  \"call_transfer_rules\": { \"timeout_seconds\": null, \"retries\": null, \"failure_message\": null },\n  \"integration_constraints\": [],\n  \"after_hours_flow_summary\": \"string or null\",\n  \"office_hours_flow_summary\": \"string or null\",\n  \"questions_or_unknowns\": [],\n  \"notes\": \"string\",\n  \"onboarding_changes_summary\": [\"list of changes with reasons\"]\n}\n\nOnboarding Transcript:\n` + onboarding_text;\n\nconst requestBody = JSON.stringify({\n  contents: [{ parts: [{ text: prompt }] }],\n  generationConfig: { temperature: 0.1, topP: 0.95, responseMimeType: 'application/json' }\n});\n\nconst result = await new Promise((resolve, reject) => {\n  const req = https.request({\n    hostname: 'generativelanguage.googleapis.com',\n    path: `/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`,\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(requestBody) },\n    timeout: 60000\n  }, (res) => {\n    let data = '';\n    res.on('data', c => data += c);\n    res.on('end', () => resolve(JSON.parse(data)));\n  });\n  req.on('error', reject);\n  req.write(requestBody);\n  req.end();\n});\n\nconst updateText = result.candidates[0].content.parts[0].text;\nconst updates = JSON.parse(updateText);\n\nreturn { json: { ...$input.item.json, updates } };"
      },
      "id": "gemini-onboarding",
      "name": "Gemini API - Extract Updates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { v1_memo, updates, account_id } = $input.item.json;\n\nfunction deepMerge(base, overlay) {\n  const result = JSON.parse(JSON.stringify(base));\n  for (const key of Object.keys(overlay)) {\n    if (key === 'onboarding_changes_summary' || key.startsWith('_')) continue;\n    const val = overlay[key];\n    if (val === null || val === undefined) continue;\n    if (Array.isArray(val)) { if (val.length > 0) result[key] = val; }\n    else if (typeof val === 'object') {\n      if (!result[key] || typeof result[key] !== 'object') result[key] = {};\n      result[key] = deepMerge(result[key], val);\n    } else { result[key] = val; }\n  }\n  return result;\n}\n\nfunction generateChangelog(v1, upd, path = '') {\n  const changes = [];\n  for (const key of Object.keys(upd)) {\n    if (key === 'onboarding_changes_summary' || key.startsWith('_')) continue;\n    const fullPath = path ? `${path}.${key}` : key;\n    const newVal = upd[key], oldVal = v1[key];\n    if (newVal === null || newVal === undefined) continue;\n    if (Array.isArray(newVal)) {\n      if (newVal.length === 0) continue;\n      const oldArr = Array.isArray(oldVal) ? oldVal : [];\n      if (JSON.stringify(oldArr) !== JSON.stringify(newVal)) changes.push({ field: fullPath, action: oldArr.length === 0 ? 'added' : 'updated', old_value: oldArr.length === 0 ? null : oldArr, new_value: newVal });\n    } else if (typeof newVal === 'object') {\n      changes.push(...generateChangelog(oldVal || {}, newVal, fullPath));\n    } else {\n      if (oldVal !== newVal) changes.push({ field: fullPath, action: oldVal == null ? 'added' : 'updated', old_value: oldVal || null, new_value: newVal });\n    }\n  }\n  return changes;\n}\n\nconst changelog = generateChangelog(v1_memo, updates);\nconst v2Memo = deepMerge(v1_memo, updates);\nv2Memo._version = 'v2';\nv2Memo._pipeline = 'onboarding';\nv2Memo._generated_at = new Date().toISOString();\n\nconst changelogDoc = { account_id, company_name: v2Memo.company_name, from_version: 'v1', to_version: 'v2', generated_at: new Date().toISOString(), total_changes: changelog.length, changes: changelog, llm_summary: updates.onboarding_changes_summary || [] };\n\nreturn { json: { account_id, v1_memo, v2_memo: v2Memo, changelog: changelogDoc } };"
      },
      "id": "diff-merge",
      "name": "Diff & Merge to v2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1000,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { v2_memo: memo, changelog } = $input.item.json;\nconst companyName = memo.company_name || 'Unknown';\nconst accountId = memo.account_id || 'unknown';\nconst bh = memo.business_hours || {};\nconst businessHoursStr = bh.days && bh.start && bh.end ? `${bh.days}, ${bh.start} - ${bh.end} ${bh.timezone||''}` : 'Not confirmed';\nconst er = memo.emergency_routing_rules || {};\nconst emergencyContacts = Array.isArray(er.who_to_call) && er.who_to_call.length > 0 ? er.who_to_call.join(', then ') : 'Not confirmed';\nconst emergencyDef = Array.isArray(memo.emergency_definition) && memo.emergency_definition.length > 0 ? memo.emergency_definition.join('; ') : 'Not defined';\nconst services = Array.isArray(memo.services_supported) && memo.services_supported.length > 0 ? memo.services_supported.join(', ') : 'General service calls';\nconst address = memo.office_address || '[Not provided]';\nconst timezone = bh.timezone || 'America/New_York';\nconst tr = memo.call_transfer_rules || {};\nconst transferTimeout = tr.timeout_seconds || 30;\nconst transferRetries = tr.retries || 2;\nconst transferFailMsg = tr.failure_message || \"I'm sorry, I wasn't able to connect you. Let me take your info.\";\nconst constraints = Array.isArray(memo.integration_constraints) && memo.integration_constraints.length > 0 ? '\\nCONSTRAINTS:\\n' + memo.integration_constraints.map(c=>`- ${c}`).join('\\n') : '';\n\nconst systemPrompt = `You are Clara, AI receptionist for ${companyName}.\\n\\nCompany: ${companyName}\\nAddress: ${address}\\nServices: ${services}\\nHours: ${businessHoursStr}\\nTimezone: ${timezone}\\n\\nBUSINESS HOURS: Greet \u2192 Determine purpose \u2192 Collect info \u2192 Route/Transfer (${transferTimeout}s, ${transferRetries} retries) \u2192 Close\\nAFTER HOURS: Greet \u2192 Emergency check \u2192 If emergency (${emergencyDef}): transfer to ${emergencyContacts} \u2192 If not: take message \u2192 Close\\n${constraints}`;\n\nconst agentSpec = { agent_name: `Clara - ${companyName}`, version: 'v2', system_prompt: systemPrompt, _generated_at: new Date().toISOString(), _source_account_id: accountId, _changelog_summary: `${changelog.total_changes} fields updated` };\n\nreturn { json: { account_id: accountId, v2_memo: $input.item.json.v2_memo, agentSpec, changelog: $input.item.json.changelog } };"
      },
      "id": "generate-spec-v2",
      "name": "Generate Spec v2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1260,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const fs = require('fs');\nconst path = require('path');\nconst { account_id, v2_memo, agentSpec, changelog } = $input.item.json;\n\nconst outputDir = path.join('/outputs', 'accounts', account_id, 'v2');\nfs.mkdirSync(outputDir, { recursive: true });\nfs.writeFileSync(path.join(outputDir, 'account_memo.json'), JSON.stringify(v2_memo, null, 2), 'utf8');\nfs.writeFileSync(path.join(outputDir, 'agent_spec.json'), JSON.stringify(agentSpec, null, 2), 'utf8');\nfs.writeFileSync(path.join(outputDir, 'changelog.json'), JSON.stringify(changelog, null, 2), 'utf8');\n\nlet md = `# Changelog: ${v2_memo.company_name}\\n**From:** v1 \u2192 **To:** v2\\n**Changes:** ${changelog.total_changes}\\n\\n`;\nfor (const c of changelog.changes) { md += `### ${c.field}\\n- Action: ${c.action}\\n- Old: ${JSON.stringify(c.old_value)}\\n- New: ${JSON.stringify(c.new_value)}\\n\\n`; }\nfs.writeFileSync(path.join(outputDir, 'changelog.md'), md, 'utf8');\n\nconst trackerPath = path.join('/outputs', 'tracker.json');\nlet tracker = { accounts: {} };\ntry { tracker = JSON.parse(fs.readFileSync(trackerPath, 'utf8')); } catch(e) {}\nif (tracker.accounts[account_id]) { tracker.accounts[account_id].status = 'v2_complete'; tracker.accounts[account_id].v2_generated_at = new Date().toISOString(); tracker.accounts[account_id].total_changes = changelog.total_changes; }\nfs.writeFileSync(trackerPath, JSON.stringify(tracker, null, 2), 'utf8');\n\nreturn { json: { success: true, account_id, company_name: v2_memo.company_name, output_dir: outputDir, files_written: ['account_memo.json','agent_spec.json','changelog.json','changelog.md'], total_changes: changelog.total_changes } };"
      },
      "id": "save-v2",
      "name": "Save v2 Outputs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const https = require('https');\nconst token = $env.ASANA_TOKEN;\nconst projectId = $env.ASANA_PROJECT_ID;\nconst prev = $input.item.json;\n\nif (!token || !projectId) return { json: { ...prev, asana_task: 'skipped' } };\n\nconst body = JSON.stringify({ data: { name: `[v2] ${prev.company_name} - Agent Updated`, projects: [projectId], notes: `Pipeline B completed. Account: ${prev.account_id}. Changes: ${prev.total_changes}` } });\n\ntry {\n  const result = await new Promise((resolve, reject) => {\n    const req = https.request({ hostname: 'app.asana.com', path: '/api/1.0/tasks', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (res) => {\n      let data = ''; res.on('data', c => data += c); res.on('end', () => resolve(JSON.parse(data)));\n    });\n    req.on('error', reject); req.write(body); req.end();\n  });\n  return { json: { ...prev, asana_task: `created (ID: ${result.data?.gid || 'unknown'})` } };\n} catch(e) { return { json: { ...prev, asana_task: `failed: ${e.message}` } }; }"
      },
      "id": "asana-v2",
      "name": "Create Asana Task (v2)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const prev = $input.item.json;\nreturn { json: { status: 'success', pipeline: 'B', account_id: prev.account_id, company_name: prev.company_name, version: 'v2', output_directory: prev.output_dir, files: prev.files_written, total_changes: prev.total_changes, asana_task: prev.asana_task, timestamp: new Date().toISOString() } };"
      },
      "id": "final-response-b",
      "name": "Build Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2040,
        300
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Read File & Load v1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read File & Load v1": {
      "main": [
        [
          {
            "node": "Gemini API - Extract Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini API - Extract Updates": {
      "main": [
        [
          {
            "node": "Diff & Merge to v2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Diff & Merge to v2": {
      "main": [
        [
          {
            "node": "Generate Spec v2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Spec v2": {
      "main": [
        [
          {
            "node": "Save v2 Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save v2 Outputs": {
      "main": [
        [
          {
            "node": "Create Asana Task (v2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Asana Task (v2)": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 1
}