{
  "name": "Pipeline A - Demo Call to v1 Agent",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "pipeline-a",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const fs = require('fs');\nconst https = require('https');\n\nconst filePath = $input.item.json.body.transcript_path;\nconst transcript = fs.readFileSync(filePath, 'utf8');\nconst apiKey = $env.GEMINI_API_KEY;\n\nif (!apiKey) throw new Error('GEMINI_API_KEY is not set in environment');\n\nconst prompt = `You are an expert data extraction assistant for Clara Answers, an AI-powered voice agent platform for service trade businesses.\n\nYour Task: Extract structured account configuration data from the following DEMO call transcript. This is an exploratory call \u2014 expect incomplete data.\n\nCRITICAL RULES:\n1. ONLY extract information EXPLICITLY stated in the transcript.\n2. NEVER invent, assume, or hallucinate any details.\n3. If a field cannot be determined, set it to null.\n4. If information is vague, add it to questions_or_unknowns and set the field to null.\n5. For a DEMO call, many fields will be null \u2014 that is correct behavior.\n\nReturn ONLY valid JSON with this structure:\n{\n  \"account_id\": \"string - derive from company name, lowercase, underscored\",\n  \"company_name\": \"string - exact company name as stated\",\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\": [\"list of services mentioned\"],\n  \"emergency_definition\": [\"list of what counts as emergency\"],\n  \"emergency_routing_rules\": { \"who_to_call\": [\"ordered list\"], \"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\": \"string or null\" },\n  \"integration_constraints\": [],\n  \"after_hours_flow_summary\": \"string or null\",\n  \"office_hours_flow_summary\": \"string or null\",\n  \"questions_or_unknowns\": [\"list things unclear or missing\"],\n  \"notes\": \"brief summary of key takeaways\"\n}\n\nTranscript:\n` + transcript;\n\nconst requestBody = JSON.stringify({\n  contents: [{ parts: [{ text: prompt }] }],\n  generationConfig: { temperature: 0.1, topP: 0.95, responseMimeType: 'application/json' }\n});\n\nfunction makeRequest() {\n  return 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', (chunk) => data += chunk);\n      res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));\n    });\n    req.on('error', reject);\n    req.write(requestBody);\n    req.end();\n  });\n}\n\nlet result;\nfor (let attempt = 1; attempt <= 3; attempt++) {\n  const resp = await makeRequest();\n  if (resp.statusCode === 429) {\n    if (attempt === 3) throw new Error('Gemini API rate limited after 3 retries. Wait 60s and try again.');\n    await new Promise(r => setTimeout(r, attempt * 15000));\n    continue;\n  }\n  try {\n    result = JSON.parse(resp.body);\n  } catch(e) {\n    throw new Error(`Gemini returned non-JSON (HTTP ${resp.statusCode}): ${resp.body.substring(0, 500)}`);\n  }\n  break;\n}\n\nif (!result.candidates || !result.candidates[0]) {\n  throw new Error('Gemini returned no candidates: ' + JSON.stringify(result).substring(0, 500));\n}\n\nconst memoText = result.candidates[0].content.parts[0].text;\nlet memo = JSON.parse(memoText);\n\nreturn { json: { memo, transcript_path: filePath } };"
      },
      "id": "read-and-extract",
      "name": "Read File & Gemini Extract",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Validate and normalize the memo\nconst memo = $input.item.json.memo;\n\nconst requiredFields = ['account_id','company_name','business_hours','office_address','services_supported','emergency_definition','emergency_routing_rules','non_emergency_routing_rules','call_transfer_rules','integration_constraints','after_hours_flow_summary','office_hours_flow_summary','questions_or_unknowns','notes'];\nfor (const f of requiredFields) { if (!(f in memo)) memo[f] = null; }\n\nconst arrayFields = ['services_supported','emergency_definition','integration_constraints','questions_or_unknowns'];\nfor (const f of arrayFields) { if (!Array.isArray(memo[f])) memo[f] = memo[f] ? [memo[f]] : []; }\n\nif (!memo.business_hours || typeof memo.business_hours !== 'object') memo.business_hours = {days:null,start:null,end:null,timezone:null};\nif (!memo.emergency_routing_rules || typeof memo.emergency_routing_rules !== 'object') memo.emergency_routing_rules = {who_to_call:[],order:null,fallback:null};\nif (!memo.call_transfer_rules || typeof memo.call_transfer_rules !== 'object') memo.call_transfer_rules = {timeout_seconds:null,retries:null,failure_message:null};\n\nmemo._version = 'v1';\nmemo._pipeline = 'demo';\nmemo._generated_at = new Date().toISOString();\nmemo._source_file = $input.item.json.transcript_path || 'unknown';\n\nreturn { json: { memo } };"
      },
      "id": "validate-memo",
      "name": "Validate Memo",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const memo = $input.item.json.memo;\nconst companyName = memo.company_name || 'Unknown Company';\nconst accountId = memo.account_id || 'unknown';\n\nconst bh = memo.business_hours || {};\nconst businessHoursStr = bh.days && bh.start && bh.end ? `${bh.days}, ${bh.start} - ${bh.end} ${bh.timezone||''}` : 'Not yet confirmed';\n\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 yet confirmed';\nconst emergencyDef = Array.isArray(memo.emergency_definition) && memo.emergency_definition.length > 0 ? memo.emergency_definition.join('; ') : 'Not yet 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 || '[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 information and have someone call you back.\";\nconst constraints = Array.isArray(memo.integration_constraints) && memo.integration_constraints.length > 0 ? '\\nIMPORTANT CONSTRAINTS:\\n' + memo.integration_constraints.map(c=>`- ${c}`).join('\\n') : '';\n\nconst systemPrompt = `You are Clara, an AI-powered receptionist for ${companyName}.\\n\\n=== COMPANY INFO ===\\nCompany: ${companyName}\\nAddress: ${address}\\nServices: ${services}\\nBusiness Hours: ${businessHoursStr}\\nTimezone: ${timezone}\\n\\n=== BUSINESS HOURS FLOW ===\\n1. GREETING: \"Thank you for calling ${companyName}, this is Clara. How can I help you?\"\\n2. DETERMINE PURPOSE\\n3. COLLECT: caller name, callback number\\n4. ROUTE OR TRANSFER\\n5. TRANSFER: Wait ${transferTimeout}s, ${transferRetries} retries. On fail: \"${transferFailMsg}\"\\n6. WRAP-UP & CLOSE\\n\\n=== AFTER-HOURS FLOW ===\\n1. GREETING: \"Thank you for calling ${companyName}. Our office is currently closed. Hours: ${businessHoursStr}.\"\\n2. EMERGENCY CHECK\\n3. IF EMERGENCY: ${emergencyDef}\\n   \u2192 Transfer to: ${emergencyContacts}\\n4. IF NOT EMERGENCY: Take message, callback next business day\\n5. CLOSE\\n\\n=== RULES ===\\n- Professional, warm, efficient\\n- Never mention internal tools\\n- Confirm info before ending call\\n${constraints}`;\n\nconst agentSpec = {\n  agent_name: `Clara - ${companyName}`, version: 'v1',\n  version_description: 'Preliminary agent from demo call',\n  voice_id: 'retell-Cimo', voice_model: 'eleven_turbo_v2',\n  language: 'en-US', responsiveness: 0.8,\n  system_prompt: systemPrompt,\n  key_variables: { company_name: companyName, timezone, business_hours: businessHoursStr, office_address: address, emergency_contacts: emergencyContacts, emergency_definition: emergencyDef, services },\n  call_transfer_protocol: { timeout_seconds: transferTimeout, max_retries: transferRetries, failure_message: transferFailMsg },\n  _generated_at: new Date().toISOString(), _source_account_id: accountId\n};\n\nreturn { json: { memo, agentSpec } };"
      },
      "id": "generate-spec",
      "name": "Generate Agent Spec",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1000,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const fs = require('fs');\nconst path = require('path');\n\nconst { memo, agentSpec } = $input.item.json;\nconst accountId = memo.account_id || 'unknown';\n\nconst outputDir = path.join('/outputs', 'accounts', accountId, 'v1');\nfs.mkdirSync(outputDir, { recursive: true });\nfs.writeFileSync(path.join(outputDir, 'account_memo.json'), JSON.stringify(memo, null, 2), 'utf8');\nfs.writeFileSync(path.join(outputDir, 'agent_spec.json'), JSON.stringify(agentSpec, null, 2), 'utf8');\n\nconst trackerPath = path.join('/outputs', 'tracker.json');\nlet tracker = {};\ntry { tracker = JSON.parse(fs.readFileSync(trackerPath, 'utf8')); } catch(e) { tracker = { accounts: {} }; }\ntracker.accounts[accountId] = { company_name: memo.company_name, status: 'v1_complete', v1_generated_at: new Date().toISOString() };\nfs.writeFileSync(trackerPath, JSON.stringify(tracker, null, 2), 'utf8');\n\nreturn { json: { success: true, account_id: accountId, company_name: memo.company_name, output_dir: outputDir, files_written: ['account_memo.json', 'agent_spec.json'], memo, agentSpec } };"
      },
      "id": "save-outputs",
      "name": "Save v1 Outputs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1260,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const https = require('https');\n\nconst token = $env.ASANA_TOKEN;\nconst projectId = $env.ASANA_PROJECT_ID;\nconst prev = $input.item.json;\n\nif (!token || !projectId) {\n  return { json: { ...prev, asana_task: 'skipped - no credentials' } };\n}\n\nconst body = JSON.stringify({ data: { name: `[v1] ${prev.company_name} - Agent Configuration`, projects: [projectId], notes: `Pipeline A completed.\\nAccount: ${prev.account_id}\\nCompany: ${prev.company_name}\\nStatus: v1 generated from demo call` } });\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) }, timeout: 10000 }, (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(body);\n    req.end();\n  });\n  const taskId = result.data ? result.data.gid : 'unknown';\n  return { json: { ...prev, asana_task: `created (ID: ${taskId})` } };\n} catch(e) {\n  return { json: { ...prev, asana_task: `failed: ${e.message}` } };\n}"
      },
      "id": "asana-create",
      "name": "Create Asana Task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const prev = $input.item.json;\nreturn { json: { status: 'success', pipeline: 'A', account_id: prev.account_id, company_name: prev.company_name, version: 'v1', output_directory: prev.output_dir, files: prev.files_written, asana_task: prev.asana_task, timestamp: new Date().toISOString() } };"
      },
      "id": "final-response",
      "name": "Build Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        300
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Read File & Gemini Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read File & Gemini Extract": {
      "main": [
        [
          {
            "node": "Validate Memo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Memo": {
      "main": [
        [
          {
            "node": "Generate Agent Spec",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Agent Spec": {
      "main": [
        [
          {
            "node": "Save v1 Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save v1 Outputs": {
      "main": [
        [
          {
            "node": "Create Asana Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Asana Task": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 1
}