AutomationFlowsGeneral › AI-Powered Webhook Data Processing

AI-Powered Webhook Data Processing

Original n8n title: Twbs

TWBS. Uses httpRequest, chainLlm, lmChatAzureOpenAi. Webhook trigger; 14 nodes.

Webhook trigger★★★★☆ complexityAI-powered14 nodesHTTP RequestChain LlmLm Chat Azure Open Ai
General Trigger: Webhook Nodes: 14 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Chainllm → HTTP Request recipe pattern — see all workflows that pair these two integrations.

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": "TWBS",
  "nodes": [
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst source = input.body && typeof input.body === 'object' && !Array.isArray(input.body)\n  ? { ...input, ...input.body }\n  : input;\n\nconst pagePresets = {\n  overview: ['protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'],\n  companyDetail: ['sites', 'powerplants', 'protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'],\n  technicalHealth: ['syncMetrics', 'devices', 'users', 'notifications'],\n  protocolExplorer: ['protocols', 'sites', 'powerplants']\n};\n\nconst defaultResources = ['sites', 'powerplants', 'protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'];\nconst defaultFilters = {\n  lang: 'en',\n  status: 'all',\n  sort: 'latest',\n  limit: 100,\n  offset: 0,\n  includeProtocolTopics: false,\n  includeProtocolData: false,\n  dateFrom: null,\n  dateTo: null,\n  siteId: null,\n  powerplantId: null,\n  templateName: null\n};\n\nconst defaultCustomer = {\n  \"key\": \"twbs\",\n  \"label\": \"TWBS\",\n  \"baseUrl\": \"http://host.docker.internal:1345/api/v1\",\n  \"token\": \"\",\n  \"environment\": \"\"\n};\n\nconst resources = Array.isArray(source.resources) && source.resources.length > 0\n  ? source.resources\n  : (source.pagePreset && pagePresets[source.pagePreset]) || defaultResources;\n\nconst customer = {\n  key: source.customerKey || source.customer || defaultCustomer.key,\n  label: source.customerLabel || source.customer || defaultCustomer.label,\n  baseUrl: String(source.baseUrl || defaultCustomer.baseUrl).replace(/\\/+$/, ''),\n  token: source.token || defaultCustomer.token || '',\n  environment: source.environment || defaultCustomer.environment || ''\n};\n\nconst authFromWebhook =\n  input.headers?.authorization ||\n  input.headers?.Authorization ||\n  source.authorization ||\n  '';\n\nreturn [{\n  json: {\n    requestedAt: new Date().toISOString(),\n    customer,\n    pagePreset: source.pagePreset || 'companyDetail',\n    resources: [...new Set(resources)],\n    filters: { ...defaultFilters, ...(source.filters || {}) },\n    resourceFilters: source.resourceFilters || {},\n    includeDebug: !!source.includeDebug,\n    authFromWebhook\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1872,
        -576
      ],
      "id": "e90270b5-b8fb-48b0-b9b1-b220b4e69325",
      "name": "Normalize Request"
    },
    {
      "parameters": {
        "jsCode": "const request = $input.first().json;\n\nconst registry = {\n  health: {\n    path: 'health',\n    auth: false,\n    buildQuery: () => ({})\n  },\n  sites: {\n    path: 'sites',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      lang: filters.lang\n    })\n  },\n  powerplants: {\n    path: 'powerplants',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      siteId: filters.siteId || undefined\n    })\n  },\n  protocols: {\n    path: 'protocols',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      status: filters.status || 'all',\n      sort: filters.sort || 'latest',\n      dateFrom: filters.dateFrom || undefined,\n      dateTo: filters.dateTo || undefined,\n      lang: filters.lang || undefined,\n      powerplantId: filters.powerplantId || undefined,\n      templateName: filters.templateName || undefined,\n      expand: filters.includeProtocolTopics ? 'topics' : undefined,\n      includeData: filters.includeProtocolData ? '1' : undefined\n    })\n  },\n  users: {\n    path: 'users',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  devices: {\n    path: 'devices',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  syncMetrics: {\n    path: 'sync-metrics',\n    auth: true,\n    buildQuery: (filters) => ({\n      dateFrom: filters.dateFrom || undefined,\n      dateTo: filters.dateTo || undefined\n    })\n  },\n  notifications: {\n    path: 'notifications',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  announcements: {\n    path: 'announcements',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  }\n};\n\nfunction compactObject(value) {\n  const output = {};\n  for (const [key, item] of Object.entries(value || {})) {\n    if (item === undefined || item === null || item === '') continue;\n    output[key] = item;\n  }\n  return output;\n}\n\nfunction toQueryString(query) {\n  return Object.entries(compactObject(query))\n    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)\n    .join('&');\n}\n\nfunction buildAuthHeader(needsAuth) {\n  if (!needsAuth) return '';\n  const rawToken = String(request.customer.token || request.authFromWebhook || '').trim();\n  if (!rawToken) {\n    throw new Error('Missing token for customer ' + request.customer.key + '.');\n  }\n  return /^Bearer\\s+/i.test(rawToken) ? rawToken : ('Bearer ' + rawToken);\n}\n\nconst items = [];\nfor (const resourceName of request.resources) {\n  const definition = registry[resourceName];\n  if (!definition) {\n    throw new Error('Unknown resource: ' + resourceName);\n  }\n\n  const filters = {\n    ...request.filters,\n    ...(request.resourceFilters?.[resourceName] || {})\n  };\n  const query = definition.buildQuery(filters);\n  const queryString = toQueryString(query);\n  const url = request.customer.baseUrl + '/' + definition.path + (queryString ? '?' + queryString : '');\n\n  items.push({\n    json: {\n      resourceName,\n      customer: request.customer,\n      requestedAt: request.requestedAt,\n      pagePreset: request.pagePreset,\n      filters,\n      query,\n      url,\n      authHeader: buildAuthHeader(definition.auth),\n      includeDebug: request.includeDebug\n    }\n  });\n}\n\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2096,
        -576
      ],
      "id": "d5776358-c398-4a41-9679-69da7ec75151",
      "name": "Build Requests"
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ $json.authHeader }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2320,
        -576
      ],
      "id": "75402c29-fe60-45fb-a3ba-21da563b383a",
      "name": "API Request"
    },
    {
      "parameters": {
        "jsCode": "const request = $('Normalize Request').first().json;\nconst builtItems = $('Build Requests').all().map((item) => item.json);\nconst responseItems = $input.all();\n\nfunction countBy(items, selector) {\n  return (items || []).reduce((accumulator, item) => {\n    const key = selector(item) || 'unknown';\n    accumulator[key] = (accumulator[key] || 0) + 1;\n    return accumulator;\n  }, {});\n}\n\nfunction templateNameValue(item) {\n  const value = item?.templateName;\n  if (typeof value === 'string') return value;\n  if (value && typeof value === 'object') {\n    return value.default || Object.values(value)[0] || 'unknown';\n  }\n  return 'unknown';\n}\n\nfunction deriveProtocols(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byStatus: countBy(items, (item) => item.status),\n    byTemplate: countBy(items, (item) => templateNameValue(item)),\n    byPowerplant: countBy(items, (item) => item.powerplantId)\n  };\n}\n\nfunction deriveUsers(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  const now = Date.parse(request.requestedAt);\n  const withinDays = (value, days) => {\n    if (!value) return false;\n    const timestamp = Date.parse(value);\n    if (Number.isNaN(timestamp)) return false;\n    return now - timestamp <= days * 24 * 60 * 60 * 1000;\n  };\n\n  return {\n    totalCount: resource?.totalCount || items.length,\n    seenLast1d: items.filter((item) => withinDays(item.lastSeenAt, 1)).length,\n    seenLast7d: items.filter((item) => withinDays(item.lastSeenAt, 7)).length,\n    seenLast30d: items.filter((item) => withinDays(item.lastSeenAt, 30)).length\n  };\n}\n\nfunction deriveDevices(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byAppVersion: countBy(items, (item) => item.appVersion),\n    byOs: countBy(items, (item) => item.os),\n    active: items.filter((item) => item.active).length,\n    inactive: items.filter((item) => !item.active).length\n  };\n}\n\nfunction deriveNotifications(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byStatus: countBy(items, (item) => item.status),\n    byType: countBy(items, (item) => item.type)\n  };\n}\n\nfunction deriveSyncMetrics(resource) {\n  const byCustomer = Array.isArray(resource?.failures?.byCustomer) ? resource.failures.byCustomer : [];\n  const byDevice = Array.isArray(resource?.failures?.byDevice) ? resource.failures.byDevice : [];\n  return {\n    environment: resource?.environment || 'unknown',\n    totalFailures: resource?.failures?.total || 0,\n    byCustomer,\n    topFailingDevices: [...byDevice].sort((left, right) => right.count - left.count).slice(0, 10)\n  };\n}\n\nconst data = {};\nconst errors = [];\n\nfor (let index = 0; index < responseItems.length; index += 1) {\n  const meta = builtItems[index];\n  const payload = responseItems[index]?.json;\n\n  if (!meta) continue;\n\n  if (meta.includeDebug && payload && typeof payload === 'object') {\n    payload._meta = {\n      url: meta.url,\n      query: meta.query,\n      limitApplied: meta.filters.limit,\n      truncated: typeof payload.totalCount === 'number' && Array.isArray(payload.items) ? payload.totalCount > payload.items.length : false\n    };\n  }\n\n  data[meta.resourceName] = payload;\n}\n\nconst derived = {};\nif (data.protocols) derived.protocols = deriveProtocols(data.protocols);\nif (data.users) derived.users = deriveUsers(data.users);\nif (data.devices) derived.devices = deriveDevices(data.devices);\nif (data.notifications) derived.notifications = deriveNotifications(data.notifications);\nif (data.syncMetrics) derived.syncMetrics = deriveSyncMetrics(data.syncMetrics);\n\nreturn [{\n  json: {\n    requestedAt: request.requestedAt,\n    customer: request.customer,\n    pagePreset: request.pagePreset,\n    resources: request.resources,\n    filters: request.filters,\n    data,\n    derived,\n    errors,\n    notes: [\n      'This HTTP Request node template fetches one page per list endpoint using the requested limit.',\n      'Set limit up to 100. If totalCount exceeds items.length, the response is truncated and should be paginated in a future workflow revision.'\n    ]\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2544,
        -576
      ],
      "id": "79453fca-af82-4d4b-ab4f-bac94c31d2d0",
      "name": "Aggregate Results"
    },
    {
      "parameters": {
        "content": "## Mock Data for now\nBecause we don't have access to their endpoints yet"
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2112,
        256
      ],
      "id": "b14412d3-b4c5-4fd6-a8af-da12660ca6a1",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "dashboarddatav2-twbs",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        1712,
        64
      ],
      "id": "d00f67ad-4b9a-4c6b-91d7-e6ecb0ff5bd0",
      "name": "Webhook"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.prompt }}",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        2608,
        192
      ],
      "id": "4e7bd807-9776-49f0-b941-53ab1b321775",
      "name": "Basic LLM Chain1"
    },
    {
      "parameters": {
        "model": "mpn-openai-sweden-gpt-5-mini",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "typeVersion": 1,
      "position": [
        2688,
        416
      ],
      "id": "e924d0bf-8062-4631-abf6-c53270c5f7f3",
      "name": "Azure OpenAI Chat Model1",
      "credentials": {
        "azureOpenAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"currentState\": \"={{ $json.currentState }}\",\n  \"prompt\": \"={{ `You are generating realistic mutation actions for an energy operations dashboard mock API.\\n\\nReturn STRICT JSON only.\\nDo not include markdown.\\nDo not include explanations.\\nDo not return the full dashboard.\\n\\nReturn this exact shape:\\n{\\n  \\\"actions\\\": []\\n}\\n\\nAllowed action types: add_user, create_protocol, transition_protocol, add_notification, add_announcement\\nAllowed protocol statuses: open, closed, exported\\nMake only 1 to 3 small realistic changes.\\n\\nCurrent dashboard state:\\n${JSON.stringify($json.currentState)}` }}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2384,
        192
      ],
      "id": "665b0c4c-b21d-4ad9-96a5-89522963b119",
      "name": "Edit Fields1"
    },
    {
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst input = $input.first().json;\nconst source = input.body && typeof input.body === 'object' && !Array.isArray(input.body)\n  ? { ...input, ...input.body }\n  : input;\nconst cacheTtlMs = Math.max(1000, Number(source.cacheTtlMs) || 30000);\n\nif (!store.dashboardStateUpdatedAtMs && store.dashboardState?.requestedAt) {\n  const parsedRequestedAt = Date.parse(store.dashboardState.requestedAt);\n  store.dashboardStateUpdatedAtMs = Number.isFinite(parsedRequestedAt) ? parsedRequestedAt : Date.now();\n}\nstore.dashboardStateRefreshingUntilMs = Number.isFinite(store.dashboardStateRefreshingUntilMs)\n  ? store.dashboardStateRefreshingUntilMs\n  : 0;\n\nif (!store.dashboardState) {\n  store.dashboardState = {\n    requestedAt: \"2026-03-12T11:45:00.000Z\",\n    customer: {\n      key: \"twbs\",\n      label: \"TWBS Energy\",\n      baseUrl: \"http://host.docker.internal:1370/api/v1\",\n      token: \"\",\n      environment: \"production\"\n    },\n    pagePreset: \"companyDetail\",\n    resources: [\n      \"sites\",\n      \"powerplants\",\n      \"protocols\",\n      \"protocolTemplates\",\n      \"groups\",\n      \"users\",\n      \"devices\",\n      \"events\",\n      \"syncMetrics\",\n      \"notifications\",\n      \"announcements\"\n    ],\n    filters: {\n      lang: \"en\",\n      status: \"all\",\n      sort: \"latest\",\n      limit: 100,\n      offset: 0,\n      includeProtocolTopics: true,\n      includeProtocolData: true,\n      dateFrom: \"2026-01-01T00:00:00Z\",\n      dateTo: \"2026-03-12T23:59:59Z\",\n      siteId: null,\n      powerplantId: null,\n      templateName: null,\n      eventName: null,\n      category: null,\n      payloadType: null\n    },\n    data: {\n      sites: {\n        items: [\n          {\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Wind Hub\",\n            customerType: \"utility\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"german\",\n            abbreviationName: \"BWH\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Windparkstrasse 12\",\n              city: \"Munich\",\n              zip: \"80331\",\n              country: \"Germany\",\n              latitude: 48.1351,\n              longitude: 11.5820,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          },\n          {\n            siteId: \"TWBS_PROD_SITE_b2Ls6O_910001002_2_HH_B2\",\n            name: \"Hamburg Solar Campus\",\n            customerType: \"commercial\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"german\",\n            abbreviationName: \"HSC\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Sonnenweg 44\",\n              city: \"Hamburg\",\n              zip: \"20095\",\n              country: \"Germany\",\n              latitude: 53.5511,\n              longitude: 9.9937,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          },\n          {\n            siteId: \"TWBS_PROD_SITE_c3Mt7P_910001003_3_NW_C3\",\n            name: \"Rhine Grid Park\",\n            customerType: \"utility\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"english\",\n            abbreviationName: \"RGP\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Energieallee 7\",\n              city: \"Cologne\",\n              zip: \"50667\",\n              country: \"Germany\",\n              latitude: 50.9375,\n              longitude: 6.9603,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          }\n        ],\n        totalCount: 3,\n        limit: 100,\n        offset: 0\n      },\n      powerplants: {\n        items: [\n          {\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Turbine Cluster A\",\n            code: \"BWH-A\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 48.1360,\n            longitude: 11.5830,\n            powerplantTypeIds: [\"wind\", \"onshore\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002102_2_WT_E5\",\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Turbine Cluster B\",\n            code: \"BWH-B\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 48.1340,\n            longitude: 11.5810,\n            powerplantTypeIds: [\"wind\", \"onshore\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\",\n            siteId: \"TWBS_PROD_SITE_b2Ls6O_910001002_2_HH_B2\",\n            name: \"Hamburg Solar Field 1\",\n            code: \"HSC-F1\",\n            hyperLinkString: \"\",\n            customerType: \"commercial\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 53.5520,\n            longitude: 9.9945,\n            powerplantTypeIds: [\"solar\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\",\n            siteId: \"TWBS_PROD_SITE_c3Mt7P_910001003_3_NW_C3\",\n            name: \"Rhine Storage Unit East\",\n            code: \"RGP-E\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 50.9383,\n            longitude: 6.9612,\n            powerplantTypeIds: [\"battery\", \"grid\"]\n          }\n        ],\n        totalCount: 4,\n        limit: 100,\n        offset: 0\n      },\n      protocols: {\n        items: [\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003401_1_AA_H1\",\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Turbine Inspection (v2)\",\n            name: \"Turbine Inspection (v2)\",\n            date: { unixOffset: 1772150000 },\n            time: \"07:00\",\n            status: \"open\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Field Engineer Schneider\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003402_2_AB_H2\",\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Turbine Inspection (v2)\",\n            name: \"Turbine Inspection (v2)\",\n            date: { unixOffset: 1772080000 },\n            time: \"09:30\",\n            status: \"closed\",\n            reportId: \"RPT-TWBS-2026-042\",\n            reports: [],\n            owner: \"Field Engineer Schneider\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003403_3_AC_H3\",\n            powerplantId: \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Solar Array Audit (v1)\",\n            name: \"Solar Array Audit (v1)\",\n            date: { unixOffset: 1772000000 },\n            time: \"11:15\",\n            status: \"closed\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Solar Lead Wagner\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003404_4_AD_H4\",\n            powerplantId: \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Storage Safety Check (v1)\",\n            name: \"Storage Safety Check (v1)\",\n            date: { unixOffset: 1771920000 },\n            time: \"14:10\",\n            status: \"exported\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Grid Ops Muller\"\n          }\n        ],\n        totalCount: 4,\n        limit: 100,\n        offset: 0\n      },\n      users: {\n        items: [\n          {\n            username: \"field-engineer-schneider\",\n            firstName: \"Lena\",\n            lastName: \"Schneider\",\n            publicParticipantId: \"PUtwbs01\",\n            lastSeenAt: \"2026-03-12T10:20:00.000Z\",\n            deviceCount: 1,\n            devices: [\n              {\n                lastConnectionDate: \"2026-03-12T10:20:00.000Z\",\n                application: \"engineeroffice\",\n                appVersion: \"121.10.0\"\n              }\n            ]\n          },\n          {\n            username: \"solar-lead-wagner\",\n            firstName: \"Tobias\",\n            lastName: \"Wagner\",\n            publicParticipantId: \"PUtwbs02\",\n            lastSeenAt: \"2026-03-12T09:00:00.000Z\",\n            deviceCount: 2,\n            devices: [\n              {\n                lastConnectionDate: \"2026-03-12T09:00:00.000Z\",\n                application: \"administration\",\n                appVersion: \"120.35.1\"\n              },\n              {\n                lastConnectionDate: \"2026-03-11T17:30:00.000Z\",\n                application: \"engineeroffice\",\n                appVersion: \"121.10.0\"\n              }\n            ]\n          }\n        ],\n        totalCount: 2,\n        limit: 100,\n        offset: 0\n      },\n      devices: {\n        items: [\n          {\n            lastConnectionDate: \"2026-03-12T10:20:00.000Z\",\n            appVersion: \"121.10.0\",\n            application: \"engineeroffice\",\n            active: true\n          },\n          {\n            lastConnectionDate: \"2026-03-12T09:00:00.000Z\",\n            appVersion: \"120.35.1\",\n            application: \"administration\",\n            active: true\n          },\n          {\n            lastConnectionDate: \"2026-03-11T17:30:00.000Z\",\n            appVersion: \"121.10.0\",\n            application: \"engineeronsite\",\n            active: true\n          }\n        ],\n        totalCount: 3,\n        limit: 100,\n        offset: 0\n      },\n      syncMetrics: {\n        environment: \"production\",\n        attemptsAndSuccessesAvailable: true,\n        failures: {\n          total: 2,\n          byCustomer: [{ customerKey: \"twbs\", count: 2 }],\n          byDevice: [\n            { deviceId: \"dev-twbs-007\", count: 1 },\n            { deviceId: \"dev-twbs-006\", count: 1 }\n          ]\n        }\n      },\n      notifications: {\n        items: [\n          {\n            id: \"notif-twbs-001\",\n            type: \"maintenance\",\n            status: \"unread\",\n            message: \"Scheduled maintenance Bavaria Wind Hub \u2013 March 18\"\n          }\n        ],\n        totalCount: 1,\n        limit: 100,\n        offset: 0\n      },\n      announcements: {\n        items: [\n          {\n            id: \"ann-twbs-001\",\n            title: \"Q1 2026 reporting deadline\",\n            publishedAt: \"2026-03-01T08:00:00.000Z\"\n          }\n        ],\n        totalCount: 1,\n        limit: 100,\n        offset: 0\n      }\n    },\n    derived: {\n      protocols: {\n        totalCount: 4,\n        byStatus: {\n          open: 1,\n          closed: 2,\n          exported: 1\n        },\n        byTemplate: {\n          \"Turbine Inspection (v2)\": 2,\n          \"Solar Array Audit (v1)\": 1,\n          \"Storage Safety Check (v1)\": 1\n        },\n        byPowerplant: {\n          \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\": 2,\n          \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\": 1,\n          \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\": 1\n        }\n      },\n      users: {\n        totalCount: 2,\n        seenLast1d: 2,\n        seenLast7d: 2,\n        seenLast30d: 2\n      },\n      devices: {\n        totalCount: 3,\n        byAppVersion: {\n          \"121.10.0\": 2,\n          \"120.35.1\": 1\n        },\n        byOs: {\n          android: 2,\n          ios: 1,\n          unknown: 0\n        },\n        active: 3,\n        inactive: 0\n      },\n      notifications: {\n        totalCount: 1,\n        byStatus: {\n          unread: 1\n        },\n        byType: {\n          maintenance: 1\n        }\n      },\n      syncMetrics: {\n        environment: \"production\",\n        totalFailures: 2,\n        byCustomer: [{ customerKey: \"twbs\", count: 2 }],\n        topFailingDevices: [\n          { deviceId: \"dev-twbs-007\", count: 1 },\n          { deviceId: \"dev-twbs-006\", count: 1 }\n        ]\n      }\n    },\n    errors: [],\n    notes: [\n      \"TWBS mock: 3 sites, 4 powerplants, 4 protocols, 2 users, 3 devices, 1 notification, 1 announcement. Germany locale, production.\",\n      \"Mock data evolves over time through AI-generated mutation actions.\"\n    ]\n  };\n}\n\nif (!Number.isFinite(store.dashboardStateUpdatedAtMs)) {\n  store.dashboardStateUpdatedAtMs = Date.now();\n}\n\nconst now = Date.now();\nconst hasState = !!store.dashboardState;\nconst cacheFresh = hasState && Number.isFinite(store.dashboardStateUpdatedAtMs)\n  ? (now - store.dashboardStateUpdatedAtMs) <= cacheTtlMs\n  : false;\nconst refreshInProgress = hasState && store.dashboardStateRefreshingUntilMs > now;\nconst serveCachedResponse = hasState && (cacheFresh || refreshInProgress);\n\nif (!serveCachedResponse) {\n  store.dashboardStateRefreshingUntilMs = now + cacheTtlMs;\n}\n\nreturn [\n  {\n    json: {\n      currentState: store.dashboardState,\n      cacheTtlMs,\n      serveCachedResponse\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        64
      ],
      "id": "0583d0c4-24ae-423d-bf90-59d31d658fa1",
      "name": "Code in JavaScript2"
    },
    {
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst state = store.dashboardState;\n\nfunction nowIso() {\n  return new Date().toISOString();\n}\n\nfunction unixNow() {\n  return Math.floor(Date.now() / 1000);\n}\n\nfunction makeId(prefix) {\n  return `${prefix}_${Date.now()}_${Math.floor(Math.random() * 10000)}`;\n}\n\nfunction pickOpenOrClosedProtocol(fromStatus) {\n  const matches = state.data.protocols.items.filter(p => p.status === fromStatus);\n  if (!matches.length) return null;\n  return matches[Math.floor(Math.random() * matches.length)];\n}\n\nfunction safeJsonParse(value) {\n  if (typeof value === 'object' && value !== null) return value;\n  if (typeof value !== 'string') return null;\n  try {\n    return JSON.parse(value);\n  } catch {\n    return null;\n  }\n}\n\nfunction ensureArray(value) {\n  return Array.isArray(value) ? value : [];\n}\n\nfunction ensureCollection(value) {\n  const collection = value && typeof value === 'object' ? value : {};\n  const items = ensureArray(collection.items);\n\n  return {\n    items,\n    totalCount: Number.isFinite(collection.totalCount) ? collection.totalCount : items.length,\n    limit: Number.isFinite(collection.limit) ? collection.limit : (state.filters?.limit ?? 100),\n    offset: Number.isFinite(collection.offset) ? collection.offset : (state.filters?.offset ?? 0)\n  };\n}\n\nfunction ensureGroupCollection(value) {\n  const collection = value && typeof value === 'object' ? value : {};\n  const items = ensureArray(collection.items);\n\n  return {\n    items,\n    totalCount: Number.isFinite(collection.totalCount) ? collection.totalCount : items.length\n  };\n}\n\nfunction ensureSyncMetrics(value) {\n  const metrics = value && typeof value === 'object' ? value : {};\n  const failures = metrics.failures && typeof metrics.failures === 'object' ? metrics.failures : {};\n  const items = ensureArray(metrics.items);\n\n  return {\n    environment: typeof metrics.environment === 'string' && metrics.environment\n      ? metrics.environment\n      : (state.customer?.environment || 'unknown'),\n    attemptsAndSuccessesAvailable: Boolean(metrics.attemptsAndSuccessesAvailable),\n    items,\n    totalCount: Number.isFinite(metrics.totalCount) ? metrics.totalCount : items.length,\n    limit: Number.isFinite(metrics.limit) ? metrics.limit : (state.filters?.limit ?? 100),\n    offset: Number.isFinite(metrics.offset) ? metrics.offset : (state.filters?.offset ?? 0),\n    dateFrom: metrics.dateFrom ?? state.filters?.dateFrom ?? null,\n    dateTo: metrics.dateTo ?? state.filters?.dateTo ?? null,\n    capped: typeof metrics.capped === 'boolean' ? metrics.capped : false,\n    maxDocsRead: Number.isFinite(metrics.maxDocsRead) ? metrics.maxDocsRead : 5000,\n    failures: {\n      total: Number.isFinite(failures.total) ? failures.total : 0,\n      byCustomer: ensureArray(failures.byCustomer),\n      byDevice: ensureArray(failures.byDevice)\n    }\n  };\n}\n\nfunction ensureDashboardShape() {\n  state.resources = Array.from(new Set([\n    ...(Array.isArray(state.resources) ? state.resources : []),\n    'sites',\n    'powerplants',\n    'protocols',\n    'protocolTemplates',\n    'groups',\n    'users',\n    'devices',\n    'events',\n    'syncMetrics',\n    'notifications',\n    'announcements'\n  ]));\n\n  const filters = state.filters && typeof state.filters === 'object' ? state.filters : {};\n  state.filters = {\n    lang: filters.lang || 'en',\n    status: filters.status || 'all',\n    sort: filters.sort || 'latest',\n    limit: Number.isFinite(filters.limit) ? filters.limit : 100,\n    offset: Number.isFinite(filters.offset) ? filters.offset : 0,\n    includeProtocolTopics: Boolean(filters.includeProtocolTopics),\n    includeProtocolData: Boolean(filters.includeProtocolData),\n    dateFrom: filters.dateFrom ?? null,\n    dateTo: filters.dateTo ?? null,\n    siteId: filters.siteId ?? null,\n    powerplantId: filters.powerplantId ?? null,\n    templateName: filters.templateName ?? null,\n    eventName: filters.eventName ?? null,\n    category: filters.category ?? null,\n    payloadType: filters.payloadType ?? null\n  };\n\n  state.data = state.data && typeof state.data === 'object' ? state.data : {};\n  state.data.protocolTemplates = ensureCollection(state.data.protocolTemplates);\n  state.data.groups = ensureGroupCollection(state.data.groups);\n  state.data.events = ensureCollection(state.data.events);\n  state.data.syncMetrics = ensureSyncMetrics(state.data.syncMetrics);\n  state.data.notifications = ensureCollection(state.data.notifications);\n  state.data.announcements = ensureCollection(state.data.announcements);\n}\n\nensureDashboardShape();\nfunction applyFallbackMutation() {\n  const message = `Auto-update: status check at ${new Date().toISOString().slice(11, 16)} UTC`;\n  state.data.notifications.items.unshift({\n    id: makeId('notif-enx'),\n    type: 'announcement',\n    status: 'unread',\n    message\n  });\n\n  if (state.data.users.items[0]) {\n    state.data.users.items[0].lastSeenAt = nowIso();\n  }\n\n  state.data.syncMetrics.failures.total += 1;\n}\n\n// LLM output can vary by node config; try a few common locations\nconst raw =\n  $json.text ??\n  $json.output ??\n  $json.response ??\n  $json.completion ??\n  $json.data ??\n  $json;\n\n// parse\nconst parsed = safeJsonParse(raw);\nlet actions = [];\n\nif (!parsed || !Array.isArray(parsed.actions)) {\n  // keep previous good state if AI output is bad, but still mutate a little\n  state.notes.unshift(`Invalid AI output ignored at ${nowIso()}`);\n} else {\n  actions = parsed.actions.slice(0, 3);\n}\n\nif (!actions.length) {\n  applyFallbackMutation();\n}\n\nfor (const action of actions) {\n  if (!action || typeof action !== 'object') continue;\n\n  if (action.type === 'add_user') {\n    const firstName = action.firstName || 'Alex';\n    const lastName = action.lastName || 'Meyer';\n    const username = `${firstName}-${lastName}`.toLowerCase().replace(/\\s+/g, '-');\n    const app = ['field', 'administration'].includes(action.application) ? action.application : 'field';\n    const version = action.appVersion || '121.10.0';\n\n    const exists = state.data.users.items.some(u => u.username === username);\n    if (!exists) {\n      const device = {\n        lastConnectionDate: nowIso(),\n        application: app,\n        appVersion: version\n      };\n\n      state.data.users.items.push({\n        username,\n        firstName,\n        lastName,\n        publicParticipantId: makeId('PUenx'),\n        lastSeenAt: nowIso(),\n        deviceCount: 1,\n        devices: [device]\n      });\n\n      state.data.devices.items.push({\n        lastConnectionDate: device.lastConnectionDate,\n        appVersion: device.appVersion,\n        application: device.application,\n        active: true\n      });\n    }\n  }\n\n  if (action.type === 'create_protocol') {\n    const powerplantExists = state.data.powerplants.items.some(\n      p => p.powerplantId === action.powerplantId\n    );\n\n    if (powerplantExists) {\n      const allowedTemplates = [\n        'Offshore Inspection (v3)',\n        'Safety Checklist (v2)',\n        'Solar Panel Audit (v1)',\n        'Emergency Repair (v1)'\n      ];\n\n      const templateName = allowedTemplates.includes(action.templateName)\n        ? action.templateName\n        : 'Offshore Inspection (v3)';\n\n      state.data.protocols.items.unshift({\n        protocolId: makeId('ENX_PROD_PRT'),\n        powerplantId: action.powerplantId,\n        protocolBriefcaseId: '',\n        templateName,\n        name: templateName,\n        date: { unixOffset: unixNow() },\n        time: new Date().toISOString().slice(11, 16),\n        status: 'open',\n        reportId: '',\n        reports: [],\n        owner: action.owner || 'Operations Team'\n      });\n    }\n  }\n\n  if (action.type === 'transition_protocol') {\n    const fromStatus = action.fromStatus;\n    const toStatus = action.toStatus;\n    const count = Math.max(1, Math.min(Number(action.count) || 1, 3));\n\n    const valid =\n      (fromStatus === 'open' && toStatus === 'closed') ||\n      (fromStatus === 'closed' && toStatus === 'exported');\n\n    if (valid) {\n      for (let i = 0; i < count; i++) {\n        const protocol = pickOpenOrClosedProtocol(fromStatus);\n        if (!protocol) break;\n\n        protocol.status = toStatus;\n        if (toStatus === 'closed' && !protocol.reportId) {\n          protocol.reportId = `RPT-ENX-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)\n            .toString()\n            .padStart(3, '0')}`;\n        }\n      }\n    }\n  }\n\n  if (action.type === 'add_notification') {\n    const notificationType = ['maintenance', 'alert', 'announcement'].includes(action.notificationType)\n      ? action.notificationType\n      : 'announcement';\n\n    const status = ['read', 'unread'].includes(action.status) ? action.status : 'unread';\n    const message = action.message || 'Operational update available';\n\n    state.data.notifications.items.unshift({\n      id: makeId('notif-enx'),\n      type: notificationType,\n      status,\n      message\n    });\n  }\n\n  if (action.type === 'add_announcement') {\n    state.data.announcements.items.unshift({\n      id: makeId('ann-enx'),\n      title: action.title || 'Operational update',\n      publishedAt: nowIso()\n    });\n  }\n}\n\n// light natural activity even without explicit AI action\nfor (const user of state.data.users.items) {\n  if (Math.random() < 0.25) {\n    user.lastSeenAt = nowIso();\n    if (Array.isArray(user.devices) && user.devices[0]) {\n      user.devices[0].lastConnectionDate = nowIso();\n    }\n  }\n}\n\nstate.requestedAt = nowIso();\n\n// totals\nstate.data.sites.totalCount = state.data.sites.items.length;\nstate.data.powerplants.totalCount = state.data.powerplants.items.length;\nstate.data.protocols.totalCount = state.data.protocols.items.length;\nstate.data.protocolTemplates.totalCount = state.data.protocolTemplates.items.length;\nstate.data.groups.totalCount = state.data.groups.items.length;\nstate.data.users.totalCount = state.data.users.items.length;\nstate.data.devices.totalCount = state.data.devices.items.length;\nstate.data.events.totalCount = state.data.events.items.length;\nstate.data.syncMetrics.totalCount = state.data.syncMetrics.items.length;\nstate.data.syncMetrics.limit = state.filters.limit;\nstate.data.syncMetrics.offset = state.filters.offset;\nstate.data.syncMetrics.dateFrom = state.data.syncMetrics.dateFrom ?? state.filters.dateFrom ?? null;\nstate.data.syncMetrics.dateTo = state.data.syncMetrics.dateTo ?? state.filters.dateTo ?? null;\nstate.data.notifications.totalCount = state.data.notifications.items.length;\nstate.data.announcements.totalCount = state.data.announcements.items.length;\n\n// derived.protocols\nconst protocolStatus = { open: 0, closed: 0, exported: 0 };\nconst protocolTemplate = {};\nconst protocolPowerplant = {};\n\nfor (const p of state.data.protocols.items) {\n  protocolStatus[p.status] = (protocolStatus[p.status] || 0) + 1;\n  protocolTemplate[p.templateName] = (protocolTemplate[p.templateName] || 0) + 1;\n  protocolPowerplant[p.powerplantId] = (protocolPowerplant[p.powerplantId] || 0) + 1;\n}\n\n// derived.users\nconst now = Date.now();\nlet seenLast1d = 0;\nlet seenLast7d = 0;\nlet seenLast30d = 0;\n\nfor (const u of state.data.users.items) {\n  const t = new Date(u.lastSeenAt).getTime();\n  const diff = now - t;\n  if (diff <= 1 * 24 * 60 * 60 * 1000) seenLast1d++;\n  if (diff <= 7 * 24 * 60 * 60 * 1000) seenLast7d++;\n  if (diff <= 30 * 24 * 60 * 60 * 1000) seenLast30d++;\n}\n\n// derived.devices\nconst byAppVersion = {};\nlet active = 0;\nlet inactive = 0;\n\nfor (const d of state.data.devices.items) {\n  byAppVersion[d.appVersion] = (byAppVersion[d.appVersion] || 0) + 1;\n  if (d.active) active++;\n  else inactive++;\n}\n\n// derived.notifications\nconst notifByStatus = {};\nconst notifByType = {};\nfor (const n of state.data.notifications.items) {\n  notifByStatus[n.status] = (notifByStatus[n.status] || 0) + 1;\n  notifByType[n.type] = (notifByType[n.type] || 0) + 1;\n}\n\nstate.derived = {\n  protocols: {\n    totalCount: state.data.protocols.items.length,\n    byStatus: protocolStatus,\n    byTemplate: protocolTemplate,\n    byPowerplant: protocolPowerplant\n  },\n  users: {\n    totalCount: state.data.users.items.length,\n    seenLast1d,\n    seenLast7d,\n    seenLast30d\n  },\n  devices: {\n    totalCount: state.data.devices.items.length,\n    byAppVersion,\n    byOs: {\n      android: Math.floor(state.data.devices.items.length / 2),\n      ios: Math.floor(state.data.devices.items.length / 2),\n      unknown: state.data.devices.items.length % 2\n    },\n    active,\n    inactive\n  },\n  notifications: {\n    totalCount: state.data.notifications.items.length,\n    byStatus: notifByStatus,\n    byType: notifByType\n  },\n  syncMetrics: {\n    environment: state.data.syncMetrics.environment,\n    totalFailures: state.data.syncMetrics.failures.total,\n    byCustomer: state.data.syncMetrics.failures.byCustomer,\n    topFailingDevices: state.data.syncMetrics.failures.byDevice\n  }\n};\n\nstate.errors = [];\nstore.dashboardState = state;\nstore.dashboardStateUpdatedAtMs = Date.now();\nstore.dashboardStateRefreshingUntilMs = 0;\n\nreturn [{ json: state }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        192
      ],
      "id": "766be527-4c05-4fc2-b9f4-a6ce23ec3347",
      "name": "Code in JavaScript3"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        3184,
        64
      ],
      "id": "adc41473-ccd0-45f5-9a44-664d84bc3981",
      "name": "Respond to Webhook"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "twbs-cache-check",
              "leftValue": "={{ $json.serveCachedResponse ? \"1\" : \"\" }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2160,
        64
      ],
      "id": "dbfd7862-553a-460f-937f-0299f94f0556",
      "name": "Use Cached Response?"
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json.cachedResponse || $input.first().json.currentState || {};\nreturn [{ json: response }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        -64
      ],
      "id": "e791c549-8a97-4174-959d-be49cfd35220",
      "name": "Return Cached Response"
    }
  ],
  "connections": {
    "Normalize Request": {
      "main": [
        [
          {
            "node": "Build Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Requests": {
      "main": [
        [
          {
            "node": "API Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API Request": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Code in JavaScript2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain1": {
      "main": [
        [
          {
            "node": "Code in JavaScript3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields1": {
      "main": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript2": {
      "main": [
        [
          {
            "node": "Use Cached Response?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Cached Response?": {
      "main": [
        [
          {
            "node": "Return Cached Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Return Cached Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "c84ca849-a0b1-4a59-95e9-276572cd1a94",
  "id": "SCKJj4jJrGbDWSqq",
  "tags": []
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

How this works

Effortlessly analyse websites for accessibility and performance issues by triggering this workflow via a simple webhook, delivering instant insights that save hours of manual testing. It's designed for web developers, UX designers, and digital agencies who need quick evaluations without deep technical expertise. The core step involves sending an HTTP request to fetch the site's data, followed by an Azure OpenAI-powered chain that processes and summarises the results into actionable recommendations.

Use this workflow for rapid audits during development sprints or client reviews, especially when integrating with tools like Azure OpenAI for AI-driven analysis. Avoid it for high-volume production monitoring, where dedicated services like Google Lighthouse offer more scalability. Common variations include customising the LLM prompts for specific compliance standards, such as WCAG, or chaining outputs to tools like Slack for team notifications.

About this workflow

TWBS. Uses httpRequest, chainLlm, lmChatAzureOpenAi. Webhook trigger; 14 nodes.

Source: https://github.com/dgales-boop/real_n8n/blob/e163c74eb15fdc99f1e6a07b952a3a1d522a31f4/n8n/Workflows/TWBS.json — 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

RoboNuggets - Faceless POV AI Machine (R24). Uses scheduleTrigger, googleSheets, chainLlm, lmChatOpenAi. Scheduled trigger; 31 nodes.

Google Sheets, Chain Llm, OpenAI Chat +5
General

Video Automation (images only). Uses chainLlm, lmChatOpenAi, outputParserStructured, splitOut. Scheduled trigger; 28 nodes.

Chain Llm, OpenAI Chat, Output Parser Structured +4
General

Main: Submit Assignment. Uses readBinaryFile, httpRequest, openAi. Webhook trigger; 22 nodes.

Read Binary File, HTTP Request, OpenAI
General

Narrating Over A Video Using Multimodal Ai. Uses lmChatOpenAi, splitOut, httpRequest, convertToFile. Event-driven trigger; 21 nodes.

OpenAI Chat, HTTP Request, Google Drive +3
General

Cv Resume Pdf Parsing With Multimodal Vision Ai. Uses manualTrigger, stickyNote, outputParserStructured, googleDrive. Event-driven trigger; 13 nodes.

Output Parser Structured, Google Drive, HTTP Request +3