AutomationFlowsGeneral › Test Ivr Call Quality Using Call-e API From a Phone Task List

Test Ivr Call Quality Using Call-e API From a Phone Task List

ByCALL-E @calle on n8n.io

This workflow manually triggers two sequential IVR quality-sample calls via the CALL-E API, validating transcripts and metadata post-call. Note: Default placeholders must be replaced with authorized E.164 test numbers before execution. Runs when you start the workflow manually.…

Event trigger★★★★☆ complexity16 nodes
General Trigger: Event Nodes: 16 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16785 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Run IVR call quality checks with CALL-E phone calls in n8n",
  "tags": [],
  "nodes": [
    {
      "id": "7696cbad-cded-463e-9db1-89e397e016e9",
      "name": "Template overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "color": 4,
        "width": 520,
        "height": 900,
        "content": "## Run IVR call quality checks with CALL-E phone calls in n8n\n\n### How it works\n\nThis workflow runs manually, loads and validates CALL-E API settings, builds a small list of IVR quality-check tasks, loops through the tasks one at a time, creates each CALL-E call with `POST /v1/calls`, polls `GET /v1/calls/{id}` until a terminal status, parses the call result, and returns a demo-friendly result view plus an execution summary.\n\n### Setup steps\n\n- Create or copy an API key from [CALL-E API Keys](https://dashboard.heycall-e.com/account/api-keys).\n- Open `CALL-E Config` and replace `replace_with_calle_api_key` with that API key.\n- Review the [CALL-E API Reference](https://docs.heycall-e.com/#/api-reference) for endpoint, request, and response details.\n- Open `Phone/Task List` and replace the blocked placeholder E.164 phone numbers with numbers you own or are explicitly authorized to call.\n- Update the IVR task text, locale, region, and metadata for your quality-check scenario.\n- Keep the workflow inactive until the API key and authorized test numbers are configured.\n- Run from `Manual Trigger` and inspect `Demo Result View` and `Execution Summary`.\n\n### Safety\n\nThis template can create real outbound calls after a valid API key and authorized phone numbers are configured. By default, placeholder numbers fail validation so the sample cannot dial accidentally. The output masks phone numbers across the complete displayed result, including metadata, raw call data, status messages, and API errors. Code-node polling is capped at four minutes so it remains below n8n's default runner task timeout.\n\n### Customization\n\nAdjust the task list, CALL-E payload construction, result schema, parsing logic, and summary formatting to match different IVR quality checks, scoring criteria, or reporting needs."
      },
      "typeVersion": 1
    },
    {
      "id": "98c1784e-67d7-4a7c-970d-6d7d7ad475f7",
      "name": "Start and configure note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 480,
        "content": "## Start and configure\n\nCreate or copy an API key from [CALL-E API Keys](https://dashboard.heycall-e.com/account/api-keys), then open `CALL-E Config` and replace `replace_with_calle_api_key`.\n\nUse the [CALL-E API Reference](https://docs.heycall-e.com/#/api-reference) for endpoint, request, and response details.\n\n`Manual Trigger` starts the workflow. `CALL-E Config` stores the API key, base URL, polling interval, and timeout. `Validate CALL-E Config` fails fast before any call task is built if the API key is missing, still set to the placeholder, or the Code-node wait timeout exceeds four minutes."
      },
      "typeVersion": 1
    },
    {
      "id": "aef54dae-9062-4408-b3ba-0ffb4af6371e",
      "name": "Prepare call batches note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1376,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 484,
        "height": 480,
        "content": "## Prepare call batches\n\n`Phone/Task List` defines the IVR quality-check rows, validates E.164 numbers, blocks the default placeholder numbers, and passes one item at a time into `Loop Over Calls` with batch size 1."
      },
      "typeVersion": 1
    },
    {
      "id": "14397733-8b48-4ce6-b310-e11047b17806",
      "name": "Create request note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        432
      ],
      "parameters": {
        "color": 7,
        "width": 552,
        "height": 480,
        "content": "## Create API request\n\n`Build CALL-E Request Payload` builds the request body, schemas, metadata, and idempotency key. `Create CALL-E Call and Wait` creates the call and polls until a terminal status or the four-minute deadline. Poll intervals and HTTP requests are bounded to stay within the n8n runner limit."
      },
      "typeVersion": 1
    },
    {
      "id": "a23c35b7-41dc-43ae-bd1b-48a60f5564af",
      "name": "Parse results note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        432
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 480,
        "content": "## Parse call results\n\n`Parse CALL-E Result` extracts call status, recipient status, attempt status, summary, transcript turns, structured result, metadata, failure code, and status signals. `Demo Result View` applies phone masking to the complete displayed result, including metadata, raw call data, status messages, and API errors."
      },
      "typeVersion": 1
    },
    {
      "id": "0bc44e59-312e-4942-8560-98d66f255645",
      "name": "Summarize execution note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 448,
        "content": "## Summarize execution\n\nThe upper branch from `Loop Over Calls` produces `Execution Summary` after all items have been processed. The summary includes counts, masked phone values, metadata round-trip data, call status, and returned summary/structured result fields."
      },
      "typeVersion": 1
    },
    {
      "id": "d463f632-8ba7-474a-9e21-16ba016c72fb",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        608,
        640
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "56fb6781-18dc-4293-a1ca-54df15c2365c",
      "name": "CALL-E Config",
      "type": "n8n-nodes-base.code",
      "position": [
        864,
        640
      ],
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      calleConfig: {\n        apiKey: \"replace_with_calle_api_key\",\n        baseUrl: \"https://api.heycall-e.com\",\n        pollIntervalSeconds: 5,\n        waitTimeoutMinutes: 4\n      }\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "19166449-3e98-4f9f-9c61-017b9c571335",
      "name": "Validate CALL-E Config",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        640
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst config = items[0]?.json?.calleConfig || {};\n\nif (!config.apiKey || config.apiKey === \"replace_with_calle_api_key\") {\n  throw new Error(\"CALL-E API key is missing. Open the CALL-E Config node and replace apiKey before running this workflow.\");\n}\n\nif (!config.baseUrl) {\n  throw new Error(\"CALL-E baseUrl is missing. Open the CALL-E Config node and set baseUrl.\");\n}\n\nconst pollIntervalSeconds = Number(config.pollIntervalSeconds);\nif (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds < 1 || pollIntervalSeconds > 30) {\n  throw new Error(\"CALL-E pollIntervalSeconds must be between 1 and 30 seconds.\");\n}\n\nconst waitTimeoutMinutes = Number(config.waitTimeoutMinutes);\nif (!Number.isFinite(waitTimeoutMinutes) || waitTimeoutMinutes <= 0 || waitTimeoutMinutes > 4) {\n  throw new Error(\"CALL-E waitTimeoutMinutes must be greater than 0 and cannot exceed 4 minutes. This keeps the Code-node polling loop below n8n\\'s default 300-second N8N_RUNNERS_TASK_TIMEOUT.\");\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "a42f8132-fc52-4960-ba1b-a9b46667a48e",
      "name": "Phone/Task List",
      "type": "n8n-nodes-base.code",
      "position": [
        1424,
        640
      ],
      "parameters": {
        "jsCode": "const config = $input.first().json.calleConfig;\n\nconst callTasks = [\n  {\n    callItemId: \"ivr_sample_001\",\n    ivrName: \"Authorized test IVR sample 1\",\n    phone: \"+1234567890\",\n    region: \"US\",\n    locale: \"en-US\",\n    task: \"Call an owned or explicitly authorized test IVR number. This is an IVR quality recording sample. Do not enter personal data, do not attempt authentication, do not make purchases, and do not request a human agent. Listen to the opening IVR/menu for up to 90 seconds. Record whether the IVR connected, detected language, menu clarity, audio quality, latency, interruptions, DTMF or speech prompts, and any transfer or retry prompts. Return a concise summary and structured IVR quality result.\",\n    metadata: {\n      lead_id: \"ivr_sample_001\",\n      notion_page_id: \"ivr_demo_sample_001\",\n      company: \"Sample IVR 1\",\n      property: \"Authorized test IVR\",\n      campaign: \"IVR QA sample\",\n      source_url: \"https://example.com/authorized-test-ivr-1\"\n    }\n  },\n  {\n    callItemId: \"ivr_sample_002\",\n    ivrName: \"Authorized test IVR sample 2\",\n    phone: \"+1234567890\",\n    region: \"US\",\n    locale: \"en-US\",\n    task: \"Call an owned or explicitly authorized test IVR number. This is an IVR quality recording sample. Do not enter personal data, do not attempt authentication, do not open a customer case, and do not request a human agent. Listen to the opening IVR/menu for up to 90 seconds. Record whether the IVR connected, detected language, menu clarity, audio quality, latency, interruptions, DTMF or speech prompts, and any transfer or retry prompts. Return a concise summary and structured IVR quality result.\",\n    metadata: {\n      lead_id: \"ivr_sample_002\",\n      notion_page_id: \"ivr_demo_sample_002\",\n      company: \"Sample IVR 2\",\n      property: \"Authorized test IVR\",\n      campaign: \"IVR QA sample\",\n      source_url: \"https://example.com/authorized-test-ivr-2\"\n    }\n  }\n];\n\nconst placeholderPhones = new Set([\"+1234567890\", \"+1234567890\"]);\nconst e164Pattern = /^\\+[1-9]\\d{7,14}$/;\n\nfor (const callTask of callTasks) {\n  if (!e164Pattern.test(callTask.phone)) {\n    throw new Error(\"Phone number for \" + callTask.callItemId + \" must be in E.164 format, for example +1234567890.\");\n  }\n\n  if (placeholderPhones.has(callTask.phone)) {\n    throw new Error(\"Replace the placeholder phone number for \" + callTask.callItemId + \" with a number you own or are explicitly authorized to call before running live calls.\");\n  }\n}\n\nreturn callTasks.map((callTask) => ({\n  json: {\n    ...callTask,\n    metadataSent: callTask.metadata,\n    calleConfig: config\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "b93bbcb6-6f7a-48f6-b009-889c186c6fcc",
      "name": "Loop Over Calls",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1712,
        640
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d28ea12c-d539-4207-9713-a96ae8082fbf",
      "name": "Build CALL-E Request Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        2000,
        752
      ],
      "parameters": {
        "jsCode": "const resultSchema = {\n  type: \"object\",\n  required: [\"ivr_reachable\", \"call_quality\", \"summary\", \"next_action\"],\n  properties: {\n    ivr_reachable: {\n      type: \"string\",\n      enum: [\"yes\", \"no\", \"unknown\"]\n    },\n    call_quality: {\n      type: \"string\",\n      enum: [\"good\", \"fair\", \"poor\", \"unknown\"]\n    },\n    summary: { type: \"string\" },\n    next_action: {\n      type: \"string\",\n      enum: [\"pass\", \"retry_later\", \"manual_review\"]\n    },\n    language_detected: { type: \"string\" },\n    menu_summary: { type: \"string\" },\n    audio_issues: { type: \"string\" },\n    latency_or_interruption_notes: { type: \"string\" },\n    dtmf_or_speech_prompt_notes: { type: \"string\" }\n  }\n};\n\nconst recipientResultSchema = {\n  type: \"object\",\n  required: [\"ivr_reachable\", \"call_quality\", \"summary\", \"next_action\"],\n  properties: resultSchema.properties\n};\n\nreturn $input.all().map((item) => {\n  const { calleConfig, metadata, metadataSent, ...callTask } = item.json;\n  const metadataPayload = metadataSent || metadata || {};\n  const createInput = {\n    task: callTask.task,\n    recipients: [\n      {\n        phones: [callTask.phone],\n        region: callTask.region || \"BR\",\n        locale: callTask.locale || \"pt-BR\"\n      }\n    ],\n    result_schema: resultSchema,\n    recipient_result_schema: recipientResultSchema,\n    metadata: {\n      ...metadataPayload,\n      source: \"n8n_two_call_task_list_sample\"\n    }\n  };\n\n  const idempotencyKey = [\n    \"n8n_list\",\n    metadataPayload.campaign || \"campaign\",\n    metadataPayload.lead_id || callTask.phone,\n    callTask.callItemId\n  ].join(\"_\").replace(/[^a-zA-Z0-9_-]/g, \"_\");\n\n  return {\n    json: {\n      ...callTask,\n      calleConfig,\n      metadataSent: createInput.metadata,\n      createInput,\n      idempotencyKey\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "ed4b9e80-93c2-41be-9c3d-093a249f0cec",
      "name": "Create CALL-E Call and Wait",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        752
      ],
      "parameters": {
        "jsCode": "const terminalStatuses = new Set([\"completed\", \"failed\", \"canceled\"]);\n\nfunction sleep(ms) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function requestJson(url, options) {\n  const response = await helpers.httpRequest({\n    url,\n    method: options.method,\n    headers: options.headers,\n    body: options.body,\n    json: true,\n    returnFullResponse: true,\n    ignoreHttpStatusErrors: true,\n    timeout: options.timeoutMs\n  });\n\n  const statusCode = response.statusCode;\n  const body = response.body;\n\n  if (statusCode < 200 || statusCode >= 300) {\n    const statusMessage = String(response.statusMessage || \"\").replace(/[\\r\\n]+/g, \" \").trim();\n    throw new Error(\"CALL-E API request failed: \" + statusCode + (statusMessage ? \" \" + statusMessage : \"\") + \". Response body omitted from workflow output.\");\n  }\n\n  return body;\n}\n\nasync function createAndWaitForCurrentItem(item) {\n  const { calleConfig, createInput, idempotencyKey, ...callTask } = item.json;\n  const apiKey = calleConfig?.apiKey;\n\n  if (!apiKey || apiKey === \"replace_with_calle_api_key\") {\n    throw new Error(\"CALL-E API key is missing. Open the CALL-E Config node and replace apiKey before running this workflow.\");\n  }\n\n  const baseUrl = (calleConfig?.baseUrl || \"https://api.heycall-e.com\").replace(/\\/$/, \"\");\n  const pollIntervalMs = Math.min(30, Math.max(1, Number(calleConfig?.pollIntervalSeconds || 5))) * 1000;\n  const timeoutMs = Math.max(1, Number(calleConfig?.waitTimeoutMinutes || 4)) * 60 * 1000;\n  const startedAtMs = Date.now();\n  let pollCount = 0;\n\n  const headers = {\n    \"Accept\": \"application/json\",\n    \"Authorization\": `Bearer ${apiKey}`,\n    \"Content-Type\": \"application/json\"\n  };\n\n  if (idempotencyKey) {\n    headers[\"Idempotency-Key\"] = idempotencyKey;\n  }\n\n  try {\n    let call = await requestJson(`${baseUrl}/v1/calls`, {\n      method: \"POST\",\n      headers,\n      body: createInput,\n      timeoutMs: 30 * 1000\n    });\n\n    while (!terminalStatuses.has(call.status)) {\n      const remainingBeforeSleepMs = timeoutMs - (Date.now() - startedAtMs);\n      if (remainingBeforeSleepMs <= 0) {\n        throw new Error(`Timed out waiting for CALL-E call ${call.id} after ${timeoutMs}ms.`);\n      }\n\n      await sleep(Math.min(pollIntervalMs, remainingBeforeSleepMs));\n      const remainingBeforePollMs = timeoutMs - (Date.now() - startedAtMs);\n      if (remainingBeforePollMs <= 0) {\n        throw new Error(`Timed out waiting for CALL-E call ${call.id} after ${timeoutMs}ms.`);\n      }\n\n      pollCount += 1;\n      call = await requestJson(`${baseUrl}/v1/calls/${call.id}`, {\n        method: \"GET\",\n        headers,\n        timeoutMs: Math.min(30 * 1000, remainingBeforePollMs)\n      });\n    }\n\n    return {\n      json: {\n        ...callTask,\n        createInput,\n        ok: true,\n        idempotencyKey,\n        pollCount,\n        rawCallResult: call,\n        apiError: null\n      }\n    };\n  } catch (error) {\n    return {\n      json: {\n        ...callTask,\n        createInput,\n        ok: false,\n        idempotencyKey,\n        pollCount,\n        rawCallResult: null,\n        apiError: {\n          message: error.message,\n          name: error.name || \"Error\"\n        }\n      }\n    };\n  }\n}\n\nreturn await Promise.all($input.all().map((item) => createAndWaitForCurrentItem(item)));"
      },
      "typeVersion": 2
    },
    {
      "id": "82a6a2ee-1053-418c-8773-2c514cc58207",
      "name": "Parse CALL-E Result",
      "type": "n8n-nodes-base.code",
      "position": [
        2608,
        752
      ],
      "parameters": {
        "jsCode": "function asArray(value) {\n  return Array.isArray(value) ? value : [];\n}\n\nfunction asObject(value) {\n  return value && typeof value === \"object\" && !Array.isArray(value) ? value : null;\n}\n\nfunction normalizeCode(value) {\n  return String(value || \"\").trim().toLowerCase().replace(/[\\s-]+/g, \"_\");\n}\n\nfunction latest(values) {\n  return values.length > 0 ? values[values.length - 1] : null;\n}\n\nreturn $input.all().map((item) => {\n  const call = asObject(item.json.rawCallResult);\n  const recipient = latest(asArray(call?.recipients));\n  const attempts = asArray(recipient?.attempts);\n  const latestAttempt = latest(attempts);\n  const transcriptTurns = attempts.flatMap((attempt) => asArray(attempt.transcript_turns));\n  const structuredResult = asObject(recipient?.structured_result) || asObject(call?.structured_result) || null;\n  const rawFailureCode = latestAttempt?.failure_code || call?.failure_code || null;\n  const failureCode = normalizeCode(rawFailureCode);\n  const callStatus = call?.status || null;\n  const recipientStatus = recipient?.status || null;\n  const latestAttemptStatus = latestAttempt?.status || null;\n  const answered = latestAttemptStatus === \"completed\" || recipientStatus === \"completed\";\n  const noAnswer = failureCode === \"no_answer\";\n  const busy = failureCode === \"busy\";\n  const failed = Boolean(item.json.apiError) || callStatus === \"failed\" || recipientStatus === \"failed\" || latestAttemptStatus === \"failed\";\n\n  return {\n    json: {\n      ...item.json,\n      callId: call?.id || null,\n      callStatus,\n      recipientStatus,\n      latestAttemptStatus,\n      failureCode: rawFailureCode,\n      failureMessage: latestAttempt?.failure_message || call?.failure_message || null,\n      metadataReturned: asObject(call?.metadata) || {},\n      summary: structuredResult?.summary || recipient?.summary || call?.summary || latestAttempt?.summary || null,\n      transcriptTurns,\n      structuredResult,\n      statusSignals: {\n        answered,\n        no_answer: noAnswer,\n        busy,\n        failed\n      },\n      taskCompleted: call?.task_completed ?? null,\n      completionConfidence: call?.completion_confidence ?? null,\n      evidence: asArray(call?.evidence)\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "1cf5d320-aecf-4d68-997e-b515c8a6555c",
      "name": "Demo Result View",
      "type": "n8n-nodes-base.code",
      "position": [
        2912,
        752
      ],
      "parameters": {
        "jsCode": "const metadataKeys = [\"lead_id\", \"notion_page_id\", \"company\", \"property\", \"campaign\"];\n\nfunction maskPhone(phone) {\n  const value = String(phone || \"\").trim();\n  if (!value) return null;\n  if (value.length <= 5) return \"****\";\n  return value.slice(0, 3) + \"****\" + value.slice(-2);\n}\n\nfunction redactPhoneData(value, parentKey = \"\", phoneContext = false) {\n  const hasPhoneContext = phoneContext || /phone/i.test(parentKey);\n\n  if (Array.isArray(value)) {\n    return value.map((entry) => redactPhoneData(entry, parentKey, hasPhoneContext));\n  }\n\n  if (value && typeof value === \"object\") {\n    return Object.fromEntries(\n      Object.entries(value).map(([key, entry]) => [key, redactPhoneData(entry, key, hasPhoneContext)])\n    );\n  }\n\n  if (typeof value === \"string\") {\n    const maskedE164 = value.replace(/\\+[1-9]\\d{7,14}/g, (phone) => maskPhone(phone));\n    if (hasPhoneContext && maskedE164 === value) return maskPhone(value);\n    return maskedE164;\n  }\n\n  if (hasPhoneContext && value !== null && (typeof value === \"number\" || typeof value === \"boolean\")) {\n    return \"****\";\n  }\n\n  return value;\n}\n\nreturn $input.all().map((item) => {\n  const result = item.json;\n  const metadataSent = result.metadataSent || {};\n  const metadataReturned = result.metadataReturned || {};\n  const missingReturnedMetadata = metadataKeys.filter((key) => metadataSent[key] !== metadataReturned[key]);\n\n  return {\n    json: redactPhoneData({\n      callItemId: result.callItemId,\n      maskedPhone: maskPhone(result.phone),\n      task: result.task,\n      metadataSent,\n      metadataReturned,\n      metadataRoundTrip: {\n        requestedKeys: metadataKeys,\n        allRequestedKeysReturned: missingReturnedMetadata.length === 0,\n        missingOrDifferentKeys: missingReturnedMetadata\n      },\n      callStatus: {\n        ok: result.ok,\n        callId: result.callId,\n        call_status: result.callStatus,\n        recipient_status: result.recipientStatus,\n        latest_attempt_status: result.latestAttemptStatus,\n        failure_code: result.failureCode,\n        failure_message: result.failureMessage,\n        answered: result.statusSignals?.answered || false,\n        no_answer: result.statusSignals?.no_answer || false,\n        busy: result.statusSignals?.busy || false,\n        failed: result.statusSignals?.failed || false\n      },\n      returnedData: {\n        summary: result.summary,\n        transcript: result.transcriptTurns,\n        structuredResult: result.structuredResult\n      },\n      redactedRawCallResult: result.rawCallResult,\n      apiError: result.apiError\n    })\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "771d3c58-fe9f-49a1-a57e-42f7cff8749f",
      "name": "Execution Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        2000,
        240
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nreturn [\n  {\n    json: {\n      processedCount: items.length,\n      successfulCount: items.filter((item) => item.json.callStatus?.ok).length,\n      failedCount: items.filter((item) => !item.json.callStatus?.ok).length,\n      results: items.map((item) => ({\n        callItemId: item.json.callItemId,\n        maskedPhone: item.json.maskedPhone,\n        task: item.json.task,\n        metadataSent: item.json.metadataSent,\n        metadataReturned: item.json.metadataReturned,\n        metadataRoundTrip: item.json.metadataRoundTrip,\n        callStatus: item.json.callStatus,\n        returnedData: item.json.returnedData\n      }))\n    }\n  }\n];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "connections": {
    "CALL-E Config": {
      "main": [
        [
          {
            "node": "Validate CALL-E Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "CALL-E Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Calls": {
      "main": [
        [
          {
            "node": "Execution Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build CALL-E Request Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Phone/Task List": {
      "main": [
        [
          {
            "node": "Loop Over Calls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Demo Result View": {
      "main": [
        [
          {
            "node": "Loop Over Calls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse CALL-E Result": {
      "main": [
        [
          {
            "node": "Demo Result View",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate CALL-E Config": {
      "main": [
        [
          {
            "node": "Phone/Task List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create CALL-E Call and Wait": {
      "main": [
        [
          {
            "node": "Parse CALL-E Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build CALL-E Request Payload": {
      "main": [
        [
          {
            "node": "Create CALL-E Call and Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow manually triggers two sequential IVR quality-sample calls via the CALL-E API, validating transcripts and metadata post-call. Note: Default placeholders must be replaced with authorized E.164 test numbers before execution. Runs when you start the workflow manually.…

Source: https://n8n.io/workflows/16785/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Reagendamiento. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 73 nodes.

Execute Workflow Trigger, Redis, N8N Nodes Evolution Api +2
General

Blotato. Uses googleSheets, @blotato/n8n-nodes-blotato. Event-driven trigger; 65 nodes.

Google Sheets, @Blotato/N8N Nodes Blotato
General

This template is a hands-on, practical exam designed to help you master n8n Expressions—the key to accessing and manipulating data in your workflows.

Stop And Error
General

This template is a hands-on, practical exam designed to test your understanding of the fundamental JSON data types. It's the perfect way to solidify your knowledge after learning the basics.

Stop And Error
General

Agendamiento. Uses n8n-nodes-evolution-api, redis, dataTable, executeWorkflowTrigger. Event-driven trigger; 60 nodes.

N8N Nodes Evolution Api, Redis, Data Table +2