AutomationFlowsSlack & Telegram › Nav Reporter

Nav Reporter

NAV Reporter. Uses postgres, telegram. Webhook trigger; 9 nodes.

Webhook trigger★★★★☆ complexity9 nodesPostgresTelegram
Slack & Telegram Trigger: Webhook Nodes: 9 Complexity: ★★★★☆ Added:

This workflow follows the Postgres → Telegram 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": "NAV Reporter",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "webhook/nav-reporter",
        "options": {}
      },
      "id": "reporter_trigger",
      "name": "NAV Reporter Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        300,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT a.*, j.title, j.company, j.url as job_url FROM applications a JOIN jobs j ON a.job_id = j.id WHERE a.user_name = 'Vitalii' AND a.nav_reported = false AND a.status = 'analyzed' ORDER BY a.submitted_at DESC LIMIT 10;",
        "options": {}
      },
      "id": "get_unreported_applications",
      "name": "Get Unreported Applications",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        500,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has_applications",
              "leftValue": "{{ $json.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt",
                "rightType": "number"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check_has_applications",
      "name": "Check Has Applications",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        700,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "// NAV.no BankID Authentication (adapted from working_final.py)\nconst playwright = require('playwright');\nconst subprocess = require('child_process');\n\nasync function authenticateWithNAV() {\n  const fnr = process.env.FN_NUMBER;\n  const password = process.env.BANKID_PWD;\n  \n  if (!fnr || !password) {\n    throw new Error('FN_NUMBER or BANKID_PWD environment variables not set');\n  }\n  \n  console.log('\ud83d\udda5\ufe0f Starting virtual display...');\n  \n  // Kill existing Xvfb processes\n  try {\n    subprocess.execSync('pkill -f Xvfb', { stdio: 'ignore' });\n  } catch (e) {\n    // Ignore if no processes to kill\n  }\n  \n  // Start Xvfb\n  const xvfbProcess = subprocess.spawn('Xvfb', [':99', '-screen', '0', '1920x1080x24', '-ac'], {\n    detached: true,\n    stdio: 'ignore'\n  });\n  \n  process.env.DISPLAY = ':99';\n  \n  // Wait for display to start\n  await new Promise(resolve => setTimeout(resolve, 2000));\n  \n  console.log('\u2705 Virtual display started');\n  \n  const browser = await playwright.chromium.launch({\n    headless: false,\n    args: ['--no-sandbox', '--disable-setuid-sandbox']\n  });\n  \n  try {\n    const page = await browser.newPage();\n    \n    console.log('\ud83c\udf10 STEP 1: Going to NAV...');\n    await page.goto('https://aktivitetsplan.nav.no/aktivitet/ny/stilling', {\n      waitUntil: 'networkidle',\n      timeout: 60000\n    });\n    \n    console.log('\u2705 STEP 2: Clicking BankID...');\n    await page.evaluate(() => {\n      const elements = document.querySelectorAll('*');\n      for (let el of elements) {\n        if ((el.textContent || '').trim() === 'BankID' && el.tagName === 'H2') {\n          el.click();\n          return;\n        }\n      }\n    });\n    \n    await page.waitForTimeout(5000);\n    await page.waitForLoadState('networkidle');\n    \n    console.log('\u2705 STEP 3: Filling FNR:', fnr.substring(0, 6) + '****');\n    await page.evaluate((fnr) => {\n      const inputs = document.querySelectorAll('input');\n      if (inputs[0]) {\n        inputs[0].value = fnr;\n        inputs[0].dispatchEvent(new Event('input', {bubbles: true}));\n      }\n    }, fnr);\n    \n    await page.waitForTimeout(2000);\n    \n    console.log('\u2705 STEP 4: Clicking Neste...');\n    await page.evaluate(() => {\n      const buttons = document.querySelectorAll('button');\n      for (let btn of buttons) {\n        if ((btn.textContent || '').toLowerCase().includes('neste')) {\n          btn.click();\n          return;\n        }\n      }\n    });\n    \n    await page.waitForTimeout(5000);\n    \n    console.log('\ud83c\udfaf STEP 5: COORDINATE CLICK on BankID-app...');\n    await page.mouse.click(640, 338);\n    \n    console.log('\u23f0 STEP 6: Waiting for modal with detection...');\n    let modalFound = false;\n    \n    for (let i = 0; i < 30; i++) {\n      await page.waitForTimeout(1000);\n      console.log(`\u23f1\ufe0f ${i+1}/30 seconds...`);\n      \n      const passwordVisible = await page.evaluate(() => {\n        const inputs = document.querySelectorAll('input[type=\"password\"]');\n        return inputs.length > 0 && inputs[0].offsetParent !== null;\n      });\n      \n      if (passwordVisible) {\n        console.log('\u2705 Password field detected!');\n        modalFound = true;\n        break;\n      }\n    }\n    \n    if (!modalFound) {\n      console.log('\u26a0\ufe0f Modal not detected, continuing anyway...');\n    }\n    \n    console.log('\ud83d\udd11 STEP 7: Filling password...');\n    \n    const passwordCount = await page.evaluate(() => {\n      const inputs = document.querySelectorAll('input[type=\"password\"]');\n      return inputs.length;\n    });\n    \n    console.log(`\ud83d\udd0d Found ${passwordCount} password fields`);\n    \n    if (passwordCount > 0) {\n      await page.evaluate((pwd) => {\n        const inputs = document.querySelectorAll('input[type=\"password\"]');\n        for (let input of inputs) {\n          input.value = pwd;\n          input.focus();\n          input.dispatchEvent(new Event('input', {bubbles: true}));\n          input.dispatchEvent(new Event('change', {bubbles: true}));\n        }\n      }, password);\n      console.log('\u2705 Password filled');\n    }\n    \n    await page.waitForTimeout(2000);\n    \n    console.log('\ud83c\udfaf STEP 8: Clicking Neste...');\n    const buttonClicked = await page.evaluate(() => {\n      const buttons = document.querySelectorAll('button');\n      for (let btn of buttons) {\n        const text = (btn.textContent || '').toLowerCase();\n        if (text.includes('neste')) {\n          btn.click();\n          return true;\n        }\n      }\n      return false;\n    });\n    \n    if (buttonClicked) {\n      console.log('\u2705 Button clicked successfully');\n    }\n    \n    console.log('\u2705 STEP 9: Waiting for final result...');\n    \n    for (let i = 0; i < 15; i++) {\n      await page.waitForTimeout(1000);\n      const currentUrl = page.url();\n      console.log(`\u23f1\ufe0f ${i+1}/15 - Current URL: ${currentUrl}`);\n      \n      if (currentUrl.includes('nav.no')) {\n        console.log('\ud83c\udf89 SUCCESS! Redirected back to NAV!');\n        break;\n      }\n    }\n    \n    const finalUrl = page.url();\n    console.log(`\ud83c\udf89 COMPLETE! Final URL: ${finalUrl}`);\n    \n    let authSuccess = false;\n    if (finalUrl.includes('nav.no')) {\n      console.log('\u2705 AUTHENTICATION SUCCESS!');\n      authSuccess = true;\n    }\n    \n    // Clean up\n    await browser.close();\n    xvfbProcess.kill();\n    subprocess.execSync('pkill -f Xvfb', { stdio: 'ignore' });\n    console.log('\ud83d\udd34 Display stopped');\n    \n    return {\n      success: authSuccess,\n      final_url: finalUrl,\n      authenticated_at: new Date().toISOString()\n    };\n    \n  } catch (error) {\n    await browser.close();\n    xvfbProcess.kill();\n    subprocess.execSync('pkill -f Xvfb', { stdio: 'ignore' });\n    throw error;\n  }\n}\n\n// Execute authentication\nreturn await authenticateWithNAV();"
      },
      "id": "authenticate_nav",
      "name": "Authenticate with NAV",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        900,
        200
      ]
    },
    {
      "parameters": {
        "functionCode": "// Submit activities to NAV for each application\nconst playwright = require('playwright');\n\nasync function submitActivitiesToNAV(applications) {\n  const browser = await playwright.chromium.launch({\n    headless: false,\n    args: ['--no-sandbox', '--disable-setuid-sandbox']\n  });\n  \n  try {\n    const page = await browser.newPage();\n    \n    // Navigate to NAV activity page\n    await page.goto('https://aktivitetsplan.nav.no/aktivitet/ny/stilling', {\n      waitUntil: 'networkidle',\n      timeout: 30000\n    });\n    \n    const submittedActivities = [];\n    \n    for (const app of applications.slice(0, 5)) { // Limit to 5 applications\n      console.log(`Submitting activity for: ${app.title} at ${app.company}`);\n      \n      try {\n        // Fill activity form\n        await page.waitForSelector('input[name=\"tittel\"], [data-testid=\"activity-title\"]', { timeout: 5000 });\n        \n        // Clear and fill title\n        await page.evaluate((title) => {\n          const titleInput = document.querySelector('input[name=\"tittel\"], [data-testid=\"activity-title\"]');\n          if (titleInput) {\n            titleInput.value = '';\n            titleInput.value = title;\n            titleInput.dispatchEvent(new Event('input', {bubbles: true}));\n          }\n        }, `S\u00f8knad: ${app.title}`);\n        \n        // Fill employer field\n        await page.evaluate((company) => {\n          const employerInput = document.querySelector('input[name=\"arbeidsgiver\"], [data-testid=\"employer\"]');\n          if (employerInput) {\n            employerInput.value = '';\n            employerInput.value = company;\n            employerInput.dispatchEvent(new Event('input', {bubbles: true}));\n          }\n        }, app.company);\n        \n        // Fill description\n        await page.evaluate((description) => {\n          const descInput = document.querySelector('textarea[name=\"beskrivelse\"], [data-testid=\"description\"]');\n          if (descInput) {\n            descInput.value = '';\n            descInput.value = description;\n            descInput.dispatchEvent(new Event('input', {bubbles: true}));\n          }\n        }, `S\u00f8kte p\u00e5 stilling: ${app.title}\\nBedrift: ${app.company}\\nS\u00f8knad sendt via: ${app.application_url}`);\n        \n        // Set date to today\n        await page.evaluate(() => {\n          const dateInput = document.querySelector('input[type=\"date\"], [data-testid=\"date\"]');\n          if (dateInput) {\n            const today = new Date().toISOString().split('T')[0];\n            dateInput.value = today;\n            dateInput.dispatchEvent(new Event('input', {bubbles: true}));\n          }\n        });\n        \n        await page.waitForTimeout(1000);\n        \n        // Submit form\n        const submitSuccess = await page.evaluate(() => {\n          const submitBtn = document.querySelector('button[type=\"submit\"], [data-testid=\"submit\"], button:contains(\"Lagre\")');\n          if (submitBtn && !submitBtn.disabled) {\n            submitBtn.click();\n            return true;\n          }\n          return false;\n        });\n        \n        if (submitSuccess) {\n          console.log(`\u2705 Activity submitted for ${app.title}`);\n          submittedActivities.push({\n            application_id: app.id,\n            job_title: app.title,\n            company: app.company,\n            submitted_to_nav: true,\n            nav_submission_time: new Date().toISOString()\n          });\n          \n          // Wait before next submission\n          await page.waitForTimeout(3000);\n          \n          // Navigate back to new activity page for next submission\n          if (applications.indexOf(app) < applications.length - 1) {\n            await page.goto('https://aktivitetsplan.nav.no/aktivitet/ny/stilling', {\n              waitUntil: 'networkidle',\n              timeout: 30000\n            });\n            await page.waitForTimeout(2000);\n          }\n        } else {\n          console.log(`\u274c Failed to submit activity for ${app.title}`);\n        }\n        \n      } catch (error) {\n        console.error(`Error submitting activity for ${app.title}:`, error);\n      }\n    }\n    \n    await browser.close();\n    \n    return {\n      success: true,\n      total_applications: applications.length,\n      submitted_count: submittedActivities.length,\n      submitted_activities: submittedActivities,\n      completed_at: new Date().toISOString()\n    };\n    \n  } catch (error) {\n    await browser.close();\n    throw error;\n  }\n}\n\n// Get applications data\nconst applications = $('Get Unreported Applications').all()[0].json;\nreturn await submitActivitiesToNAV(applications);"
      },
      "id": "submit_nav_activities",
      "name": "Submit NAV Activities",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1100,
        200
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Update applications as reported to NAV\nUPDATE applications \nSET nav_reported = true, \n    status = 'nav_reported',\n    nav_reported_at = NOW()\nWHERE id IN ({{ $json.submitted_activities.map(item => item.application_id).join(', ') }});\n\n-- Return updated applications\nSELECT a.*, j.title, j.company \nFROM applications a \nJOIN jobs j ON a.job_id = j.id \nWHERE a.id IN ({{ $json.submitted_activities.map(item => item.application_id).join(', ') }});",
        "options": {}
      },
      "id": "update_nav_status",
      "name": "Update NAV Status",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1300,
        200
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "{{ $env.TELEGRAM_CHAT_ID }}",
        "text": "\ud83d\udccb NAV Reporting Complete\\n\\n\u2705 Authentication: Successful\\n\ud83d\udcca Applications processed: {{ $json.total_applications }}\\n\ud83d\udcdd Activities submitted: {{ $json.submitted_count }}\\n\\n\ud83d\udccb Submitted to NAV:\\n{{ $json.submitted_activities.map(item => `\u2022 ${item.job_title} (${item.company})`).join('\\n') }}\\n\\n\ud83d\udd52 Completed: {{ $now.format('HH:mm DD/MM/YYYY') }}"
      },
      "id": "send_nav_report",
      "name": "Send NAV Report",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1500,
        200
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "{{ $env.TELEGRAM_CHAT_ID }}",
        "text": "\u2139\ufe0f NAV Reporter - No Applications\\n\\nNo unreported applications found.\\nAll submitted applications have been reported to NAV.\\n\\n\ud83d\udd52 {{ $now.format('HH:mm DD/MM/YYYY') }}"
      },
      "id": "send_no_applications",
      "name": "Send No Applications",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        900,
        400
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "{{ $env.TELEGRAM_CHAT_ID }}",
        "text": "\ud83d\udea8 NAV Authentication Failed\\n\\n\u274c Could not authenticate with BankID\\n\ud83d\udd27 Manual intervention required\\n\\n\ud83d\udcdd Error details available in logs\\n\ud83d\udd52 {{ $now.format('HH:mm DD/MM/YYYY') }}"
      },
      "id": "send_auth_error",
      "name": "Send Auth Error",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1100,
        400
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "NAV Reporter Trigger": {
      "main": [
        [
          {
            "node": "Get Unreported Applications",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Unreported Applications": {
      "main": [
        [
          {
            "node": "Check Has Applications",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Has Applications": {
      "main": [
        [
          {
            "node": "Authenticate with NAV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send No Applications",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Authenticate with NAV": {
      "main": [
        [
          {
            "node": "Submit NAV Activities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit NAV Activities": {
      "main": [
        [
          {
            "node": "Update NAV Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update NAV Status": {
      "main": [
        [
          {
            "node": "Send NAV Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner"
  },
  "staticData": null,
  "tags": [
    {
      "createdAt": "2025-01-19T22:00:00.000Z",
      "updatedAt": "2025-01-19T22:00:00.000Z",
      "id": "jobhunter",
      "name": "jobhunter"
    }
  ],
  "triggerCount": 0,
  "updatedAt": "2025-01-19T22:00:00.000Z",
  "versionId": "1"
}

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

About this workflow

NAV Reporter. Uses postgres, telegram. Webhook trigger; 9 nodes.

Source: https://github.com/SmmShaman/jobbot-norway-public/blob/04db9f51b6c604268bd2be177806f9186ec89e4e/n8n-workflows/05_nav_reporter.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

This template is ideal for users managing Telegram channels who want to automate the process of posting messages, including text and images, directly from an bot.

Telegram Trigger, Postgres, Telegram
Slack & Telegram

This template is for developers and automation specialists looking to create a Telegram bot that enables users to select items from a predefined list and save their choices to a Postgres database.

Telegram, Postgres, Telegram Trigger
Slack & Telegram

This workflow is for Telegram bot developers or marketers who want to verify user subscriptions to specific Telegram channels and optionally reward them with downloadable files.

Telegram, Postgres, Google Drive +1
Slack & Telegram

This workflow is ideal for businesses or individuals who want to run Telegram-based giveaways that require users to subscribe to certain channels in order to participate.

Telegram, Postgres, Telegram Trigger
Slack & Telegram

Advanced Slackbot With N8N. Uses slack, httpRequest, stickyNote, executeWorkflow. Webhook trigger; 34 nodes.

Slack, HTTP Request, Execute Workflow Trigger +1