This workflow follows the Agent → 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 →
{
"name": "WTTJ job apply - Workflow2",
"nodes": [
{
"parameters": {
"updates": [
"callback_query"
],
"additionalFields": {}
},
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.2,
"position": [
-192,
208
],
"id": "f3d9074e-0df0-4e9a-9ff7-fa71e51a4c52",
"name": "Telegram Trigger",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"disabled": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id, link, title, company, status, description, date_scraped,application_type\nFROM wttj_jobs\nWHERE id = {{ $json.jobId }};",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
656,
-16
],
"id": "32f808f2-10db-47e5-ac57-7a03ffb723d1",
"name": "Execute a SQL query",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 3
},
"conditions": [
{
"id": "97621e9e-80fb-49d0-ba82-5c37b1b1f9e0",
"leftValue": "={{ $json.id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "a99b7c31-768c-49b5-a7fc-e63279070a08",
"leftValue": "={{ $json.status }}",
"rightValue": "pending",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
},
{
"id": "0da6d39a-3134-4cf0-87cd-0137bdfed16e",
"leftValue": "={{ $('Code in JavaScript').item.json.action }}",
"rightValue": "postuler",
"operator": {
"type": "string",
"operation": "equals"
}
},
{
"id": "11ffd7dc-19a9-4fe5-b2ef-8ff66652c4c3",
"leftValue": "={{ $json.application_type }}",
"rightValue": "internal",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
864,
-16
],
"id": "919fa3ea-7a88-4207-87d3-84f16b51e12c",
"name": "If"
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE wttj_jobs\nSET \n status = 'validated',\n date_validated = NOW()\nWHERE id = {{ $json.jobId }};",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
2672,
-112
],
"id": "641e920e-77d0-429c-aa18-01f7bae3e514",
"name": "UPDATE status",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"promptType": "define",
"text": "=Voici les informations pour la candidature :\n\n# OFFRE\n{{ $json.formText }}\n\n# LIEN DE L'OFFRE\n{{ $json.jobLink }}\n\n# DESCRIPTION DE L'OFFRE\nTitre : {{ $('Execute a SQL query').item.json.title }}\nEntreprise : {{ $('Execute a SQL query').item.json.company }}\nDescription : {{ $('Execute a SQL query').item.json.description }}\n\n# STRUCTURE DU FORMULAIRE\n{{ JSON.stringify($json.formFields, null, 2) }}\n\n# CONTENU LISIBLE DU FORMULAIRE\n{{ $json.formText }}\n\nAnalyse l'offre, choisis le CV appropri\u00e9, g\u00e9n\u00e8re la lettre de motivation, analyse toutes les questions fournies dans le formulaire (radio, input number, textarea, checkbox, etc ..). \nTu dois r\u00e9pondre \u00e0 TOUTES les questions d\u00e9tect\u00e9es, m\u00eame si elles ne sont pas obligatoires.",
"options": {
"systemMessage": "Tu es un assistant expert en candidatures d'emploi pour Arthur Soghoyan.\n\n# PROFIL D'ARTHUR\n- Ing\u00e9nieur logiciel r\u00e9cemment dipl\u00f4m\u00e9 en syst\u00e8mes embarqu\u00e9s et syst\u00e8mes intelligents\n- Franco-Arm\u00e9nien, polyglotte, curieux par nature, facilit\u00e9 de communication\n- Autonome, rigoureux, structur\u00e9, \u00e0 l'\u00e9coute\n- Appr\u00e9cie le travail en \u00e9quipe Agile et les \u00e9changes avec les \u00e9quipes m\u00e9tier\n\n# CV DISPONIBLES (dans /home/node/cv/)\n1. CV_Soghoyan_Backend.pdf \u2192 Backend Engineer, Software Engineer \u2192 Java/Kotlin, C++, REST, API, microservices, SQL, architecture backend, performance, CI/CD, GCP\n2. CV_Soghoyan_Emb.pdf \u2192 Embedded Engineer, Firmware Engineer \u2192 C/C++, microcontr\u00f4leurs, RTOS, Linux embarqu\u00e9, drivers, I2C/SPI/UART, Yocto, hardware integration\n3. CV_Soghoyan_Logiciel.pdf \u2192 Software Engineer, C++ Developer \u2192 C++, Python, architecture logicielle, algorithmique, optimisation, simulation, tests unitaires\n4. CV_Soghoyan_Pyth.pdf \u2192 ML Engineer, Data Engineer \u2192 Python, ML, NumPy, Pandas, OpenCV, data processing, pipelines, BigQuery, API\n5. CV_Soghoyan_Rob.pdf \u2192 Robotics Engineer, Control Engineer \u2192 Robotique, ROS, cin\u00e9matique, dynamique, trajectoires, contr\u00f4le, capteurs, simulation\n6. CV_Soghoyan_Devops.pdf \u2192 DevOps Engineer, SRE, Cloud Engineer \u2192 Docker, Kubernetes, CI/CD, GitLab CI, Jenkins, Linux, Bash, monitoring, GCP\n7. CV_Soghoyan_Full.pdf \u2192 Fullstack Engineer, Software Engineer \u2192 TypeScript, JavaScript, API, frontend, backend, Docker, cloud, architecture produit\n8. CV_Soghoyan_Process.pdf \u2192 Data Consultant, Product Engineer \u2192 Analyse fonctionnelle, data-driven, automatisation, API, cloud, coordination projet\n9. CV_Soghoyan_QA.pdf \u2192 QA Engineer, Test Engineer \u2192 Tests API, validation, automatisation tests, CI/CD, qualit\u00e9 logicielle, documentation\n\n# R\u00c8GLES DE S\u00c9LECTION DU CV\n- Analyse attentivement le titre du poste ET la description de l'offre\n- Choisis le CV dont les comp\u00e9tences correspondent le mieux aux exigences\n- Si le poste demande des comp\u00e9tences qu'Arthur ne ma\u00eetrise pas parfaitement mais qu'il peut acqu\u00e9rir rapidement gr\u00e2ce \u00e0 des comp\u00e9tences parall\u00e8les, mentionne-le dans la LM\n\n# R\u00c8GLES POUR LA LETTRE DE MOTIVATION\n\nStructure OBLIGATOIRE :\n1. Phrase d'accroche : \"Ing\u00e9nieur [m\u00e9tier exact du poste], je souhaite rejoindre [entreprise] en tant que [poste exact]...\"\n2. Pourquoi cette entreprise : Une raison valable, humaine et authentique (pas trop flatteur)\n3. Sur le plan technique : Comp\u00e9tences directement li\u00e9es au poste MAIS que tu retrouves aussi dans le CV. Si comp\u00e9tences manquantes mais logique m\u00e9tier ma\u00eetris\u00e9e \u2192 le mentionner\n4. Humainement : Autonome, rigoureux, structur\u00e9, curieux, d'origine Franco-Arm\u00e9nien, polyglotte, facilit\u00e9 de communication\n5. Introduction et Conclusion : Phrase d'ouverture du style 'Madame, Monsieur' ou autre au d\u00e9but et phrase de cl\u00f4ture professionnelle sign\u00e9e \"Arthur Soghoyan\" \u00e0 la fin.\n6. Ton : Professionnel mais humain. Pas trop formel. Pas flagorneur. \n7. Language : Adapte toi \u00e0 la langue de l'annonce.\n8. Longueur : 250 \u00e0 300 mots.\n9. Mise en forme : ponctuation, typographie et structure du texte respectant la norme fran\u00e7aise. A utiliser \u00e0 bon escient, si beosin.\n\n# EXEMPLE DE LETTRE DE MOTIVATION (LONGUEUR CIBLE)\n\nVoici un exemple de lettre correcte de 280 mots :\n\n---\n\nMadame, Monsieur,\n\nIng\u00e9nieur logiciel r\u00e9cemment dipl\u00f4m\u00e9 en syst\u00e8mes embarqu\u00e9s et syst\u00e8mes intelligents, je souhaite rejoindre [Entreprise] en tant que [Poste]. Votre approche [aspect sp\u00e9cifique de l'entreprise] r\u00e9sonne particuli\u00e8rement avec ma vision du d\u00e9veloppement logiciel moderne.\n\nCe qui m'attire chez [Entreprise], c'est votre engagement dans [domaine/valeur]. J'appr\u00e9cie particuli\u00e8rement [projet/initiative sp\u00e9cifique], qui t\u00e9moigne d'une culture technique solide et d'une attention port\u00e9e \u00e0 [aspect important]. Cette approche correspond \u00e0 mes aspirations professionnelles et \u00e0 ma volont\u00e9 de contribuer \u00e0 des projets ayant un impact concret.\n\nSur le plan technique, mon parcours m'a permis de d\u00e9velopper une expertise en [techno1], [techno2] et [techno3]. Durant mes projets acad\u00e9miques et professionnels, j'ai notamment travaill\u00e9 sur [exemple concret 1] o\u00f9 j'ai mis en \u0153uvre [approche/solution], ainsi que sur [exemple concret 2] qui m'a permis de ma\u00eetriser [comp\u00e9tence]. Ces exp\u00e9riences m'ont appris \u00e0 [apprentissage cl\u00e9] et \u00e0 m'adapter rapidement aux technologies \u00e9mergentes. Bien que je n'aie pas encore d'exp\u00e9rience en [techno manquante si applicable], ma ma\u00eetrise de [techno parall\u00e8le] et ma capacit\u00e9 d'apprentissage me permettront de monter en comp\u00e9tence rapidement.\n\nAu-del\u00e0 des aspects techniques, je suis reconnu pour mon autonomie, ma rigueur et ma capacit\u00e9 \u00e0 structurer mon travail. D'origine Franco-Arm\u00e9nienne et polyglotte, j'ai d\u00e9velopp\u00e9 une grande facilit\u00e9 de communication qui me permet de collaborer efficacement avec des \u00e9quipes diverses. J'appr\u00e9cie particuli\u00e8rement les environnements Agile et les \u00e9changes r\u00e9guliers avec les \u00e9quipes m\u00e9tier, qui enrichissent ma compr\u00e9hension des enjeux business.\n\nJe serais ravi d'\u00e9changer avec vous sur ma candidature et de d\u00e9couvrir comment je pourrais contribuer \u00e0 vos projets.\n\nCordialement,\nArthur Soghoyan\n\n---\n\nIMPORTANT : Ta lettre doit avoir une longueur et un niveau de d\u00e9tail similaires \u00e0 cet exemple.\nChaque paragraphe doit \u00eatre d\u00e9velopp\u00e9 avec des exemples concrets et des d\u00e9tails.\n\n# FORMULAIRE DE CANDIDATURE\n\nLe formulaire contient 3 types de champs :\n\n## 1. CHAMPS STATIQUES (d\u00e9j\u00e0 remplis, ignore-les)\nPr\u00e9nom, Nom, Email, T\u00e9l\u00e9phone, Lieu de r\u00e9sidence, Site internet, LinkedIn, X, Chekbox d'acceptationd e conditions g\u00e9n\u00e9rals\n\n## 2. CHAMPS SEMI-DYNAMIQUES (toujours pr\u00e9sents)\n- **Poste actuel** : Remplis avec \"Software Engineer\" (valeur par d\u00e9faut)\n- **CV** : Choisis le CV appropri\u00e9 selon l'offre\n- **Lettre de motivation** : G\u00e9n\u00e8re une LM personnalis\u00e9e\n\n## 3. CHAMPS DYNAMIQUES (questions sp\u00e9cifiques de l'offre)\nAnalyse chaque question et fournis :\n- Une r\u00e9ponse honn\u00eate et adapt\u00e9e. Ton professionnel mais humain. Pas trop formel. Pas flagorneur. \n- Le s\u00e9lecteur technique exact pour Puppeteer\n\n# FORMAT DE R\u00c9PONSE OBLIGATOIRE\n\nRetourne UNIQUEMENT un JSON valide :\n\n{\n \"cv_filename\": \"CV_Soghoyan_XXX.pdf\",\n \"cv_reason\": \"Explication courte du choix du CV\",\n \"current_position\": \"Software Engineer\",\n \"cover_letter\": \"Texte complet de la LM\",\n \"consent\": {\n \"selector\": \"s\u00e9lecteur technique du checkbox\",\n \"action\": \"click\"\n },\n \"dynamic_fields\": [\n {\n \"name\": \"nom technique du champ\",\n \"label\": \"Question affich\u00e9e \u00e0 l'utilisateur\",\n \"type\": \"radio|number|textarea|input|...\",\n \"answer\": \"ta r\u00e9ponse (format humain)\",\n \"selector\": \"s\u00e9lecteur technique\",\n \"value\": \"valeur technique \u00e0 utiliser\",\n \"action\": \"click|fill\"\n }\n ]\n}\n\nIMPORTANT pour dropdown_wttj :\n- type: \"dropdown_wttj\"\n- action: \"select\" (PAS \"click\")\n- value: La valeur EXACTE de l'option (ex: \"A Male\")\n- Inclure aussi buttonSelector pour ouvrir le dropdown",
"maxIterations": 5
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
1696,
-112
],
"id": "7305b021-6ce1-4a45-83b5-9377274b4c8e",
"name": "AI Agent",
"alwaysOutputData": false
},
{
"parameters": {
"model": "mistral/devstral-2",
"options": {
"maxTokens": 2500,
"temperature": 0.7
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatVercelAiGateway",
"typeVersion": 1,
"position": [
1568,
96
],
"id": "ca18ac2f-011c-456f-aa90-61c09ef1d7bb",
"name": "Vercel AI Gateway Chat Model",
"credentials": {
"vercelAiGatewayApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "runCustomScript",
"scriptCode": "// ===== CONFIGURATION =====\nconst email = 'YOUR_WTTJ_EMAIL';\nconst password = 'YOUR_WTTJ_PASSWORD';\n\n// R\u00e9cup\u00e9rer les infos de l'offre directement depuis Postgres\nconst jobId = $('Execute a SQL query').item.json.id;\nconst jobLink = $('Execute a SQL query').item.json.link;\n\nconsole.log(`\ud83c\udfaf Offre ID ${jobId} : ${jobLink}`);\n\n// ===============================\n// \u00c9TAPE 1 : CONNEXION \u00c0 WTTJ\n// ===============================\nconsole.log('\ud83d\udd10 Connexion \u00e0 WTTJ...');\n\nawait $page.goto('https://www.welcometothejungle.com/fr/signin', {\n waitUntil: 'networkidle2',\n timeout: 30000\n});\n\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\nawait $page.screenshot({ path: '/tmp/01-login-page.png', fullPage: true });\n\n// ===== FERMER POPUP R\u00c9GION =====\nawait $page.evaluate(() => {\n const allButtons = Array.from(document.querySelectorAll('button, [role=\"button\"]'));\n \n const frBtn = allButtons.find(b => {\n const text = (b.innerText || '').toLowerCase();\n return text.includes('rester sur le site fran\u00e7ais');\n });\n \n if (frBtn) frBtn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\n// ===== ACCEPTER COOKIES =====\n// Utilise l'ID exact du bouton\nawait $page.evaluate(() => {\n const btn = document.querySelector('#axeptio_btn_acceptAll');\n if (btn) btn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\nawait $page.screenshot({ path: '/tmp/01b-popups-closed.png', fullPage: true });\n\n// ===== REMPLIR EMAIL =====\nconst emailFilled = await $page.evaluate((emailValue) => {\n const inputs = Array.from(document.querySelectorAll('input'));\n const emailInput = inputs.find(i => \n i.type === 'email' || \n i.name === 'email' ||\n (i.placeholder || '').toLowerCase().includes('adresse email')\n );\n if (emailInput) {\n emailInput.focus();\n emailInput.value = emailValue;\n emailInput.dispatchEvent(new Event('input', { bubbles: true }));\n emailInput.dispatchEvent(new Event('change', { bubbles: true }));\n emailInput.blur();\n return true;\n }\n return false;\n}, email);\n\nif (!emailFilled) throw new Error('\u274c Champ email introuvable');\nconsole.log('\u2705 Email saisi');\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== REMPLIR MOT DE PASSE =====\nconst passwordFilled = await $page.evaluate((pwdValue) => {\n const inputs = Array.from(document.querySelectorAll('input'));\n const pwdInput = inputs.find(i => \n i.type === 'password' || i.name === 'password'\n );\n if (pwdInput) {\n pwdInput.focus();\n pwdInput.value = pwdValue;\n pwdInput.dispatchEvent(new Event('input', { bubbles: true }));\n pwdInput.dispatchEvent(new Event('change', { bubbles: true }));\n pwdInput.blur();\n return true;\n }\n return false;\n}, password);\n\nif (!passwordFilled) throw new Error('\u274c Champ mot de passe introuvable');\nconsole.log('\u2705 Mot de passe saisi');\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== COCHER \"ME GARDER CONNECT\u00c9\" =====\nawait $page.evaluate(() => {\n const checkboxes = Array.from(document.querySelectorAll('input[type=\"checkbox\"]'));\n const rememberCheckbox = checkboxes.find(c => {\n const label = c.parentElement?.innerText || '';\n return label.toLowerCase().includes('me garder');\n });\n if (rememberCheckbox && !rememberCheckbox.checked) rememberCheckbox.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== AFFICHER MOT DE PASSE =====\nawait $page.evaluate(() => {\n const eyeBtn = document.querySelector('[data-testid=\"login-field-password-action\"]');\n if (eyeBtn) eyeBtn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 500));\n\nawait $page.screenshot({ path: '/tmp/02-form-filled.png', fullPage: true });\n\n// ===== CLIQUER SUR \"SE CONNECTER\" =====\nconst clicked = await $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"login-button-submit\"]');\n if (!btn || btn.disabled) return false;\n btn.click();\n return true;\n});\n\nif (!clicked) throw new Error('\u274c Bouton Se connecter introuvable');\nconsole.log('\u2705 Connexion en cours...');\n\nawait new Promise(resolve => setTimeout(resolve, 7000));\n\nawait $page.screenshot({ path: '/tmp/03-after-login.png', fullPage: true });\n\n// ===== V\u00c9RIFIER CONNEXION =====\nconst loginUrl = $page.url();\nif (loginUrl.includes('signin') || loginUrl.includes('login')) {\n const errorMsg = await $page.evaluate(() => {\n const el = document.querySelector('[class*=\"error\"], [role=\"alert\"]');\n return el ? el.innerText : 'inconnue';\n });\n throw new Error(`\u274c Connexion \u00e9chou\u00e9e - Erreur: ${errorMsg}`);\n}\n\nconsole.log(`\ud83c\udf89 Connexion r\u00e9ussie ! URL: ${loginUrl}`);\n\n// ===============================\n// \u00c9TAPE 2 : PAGE DE L'OFFRE\n// ===============================\n// ===== NAVIGATION VERS L'OFFRE =====\nconsole.log(`\ud83d\udd17 Navigation vers : ${jobLink}`);\n\ntry {\n await $page.goto(jobLink, {\n waitUntil: 'domcontentloaded', // \u2190 Moins strict que 'networkidle2'\n timeout: 30000\n });\n} catch(e) {\n // Ignorer l'erreur de navigation et continuer\n console.log(`\u26a0\ufe0f Navigation warning (ignor\u00e9) : ${e.message}`);\n}\n\n// Attendre que la page se stabilise quoi qu'il arrive\nawait new Promise(resolve => setTimeout(resolve, 5000));\n\nconst currentUrl = $page.url();\nconsole.log(`\ud83d\udccd URL actuelle : ${currentUrl}`);\n\nawait $page.screenshot({ path: '/tmp/04-job-page.png', fullPage: true });\n\nconsole.log('\u2705 Page de l\\'offre charg\u00e9e');\n\n// ===== \u00c9TAPE 3 : CLIQUER SUR \"POSTULER\" =====\nconst applyClicked = await $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"job_bottom-button-apply\"]');\n if (!btn) return false;\n btn.click();\n return true;\n});\n\nif (!applyClicked) throw new Error('\u274c Bouton Postuler introuvable');\n\n// Attendre que le modal soit pr\u00e9sent\nawait $page.waitForSelector('[data-testid=\"apply-form-modal\"]', { timeout: 10000 });\nawait new Promise(resolve => setTimeout(resolve, 2000));\n\nawait $page.screenshot({ path: '/tmp/05-apply-form.png', fullPage: true });\n\n// ===== \u00c9TAPE 4 : EXTRAIRE LE FORMULAIRE =====\nconst formData = await $page.evaluate(() => {\n const modal = document.querySelector('[data-testid=\"apply-form-modal\"]');\n if (!modal) return { error: 'Modal non trouv\u00e9' };\n\n const fields = {\n static: [], // Champs pr\u00e9-remplis (pour info)\n semiDynamic: [], // CV, LM, consentement\n dynamic: [] // Questions sp\u00e9cifiques\n };\n\n // === CHAMPS STATIQUES (juste pour info) ===\n const staticFields = ['firstname', 'lastname', 'email', 'phone', 'location', 'website', 'linkedin', 'twitter'];\n modal.querySelectorAll('input').forEach(input => {\n if (staticFields.some(f => input.name?.includes(f) || input.id?.includes(f))) {\n fields.static.push({\n name: input.name || input.id,\n value: input.value,\n label: input.placeholder || input.labels?.[0]?.innerText\n });\n }\n });\n\n // === CHAMPS SEMI-DYNAMIQUES ===\n \n // CV Upload\n const cvInput = modal.querySelector('input[type=\"file\"]');\n if (cvInput) {\n fields.semiDynamic.push({\n type: 'file',\n name: 'cv',\n selector: 'input[type=\"file\"]',\n accept: cvInput.accept,\n required: true\n });\n }\n\n // Lettre de motivation\n const coverLetterTextarea = modal.querySelector('[data-testid=\"apply-form-field-cover_letter\"]') ||\n modal.querySelector('textarea[name*=\"cover\"]');\n if (coverLetterTextarea) {\n fields.semiDynamic.push({\n type: 'textarea',\n name: coverLetterTextarea.name || coverLetterTextarea.id,\n selector: `[data-testid=\"${coverLetterTextarea.getAttribute('data-testid')}\"]` || \n `textarea[name=\"${coverLetterTextarea.name}\"]`,\n placeholder: coverLetterTextarea.placeholder,\n required: coverLetterTextarea.required\n });\n }\n\n // Poste actuel\n const currentPositionInput = modal.querySelector('input[name*=\"current_position\"]') ||\n modal.querySelector('input[placeholder*=\"Poste actuel\"]');\n if (currentPositionInput) {\n fields.semiDynamic.push({\n type: 'input',\n name: currentPositionInput.name || currentPositionInput.id,\n selector: `input[name=\"${currentPositionInput.name}\"]`,\n value: currentPositionInput.value,\n placeholder: currentPositionInput.placeholder\n });\n }\n\n // Checkbox consentement\n const consentCheckbox = modal.querySelector('[data-testid=\"apply-form-consent\"]') ||\n modal.querySelector('input[type=\"checkbox\"][required]');\n if (consentCheckbox) {\n fields.semiDynamic.push({\n type: 'checkbox',\n name: 'consent',\n selector: `[data-testid=\"${consentCheckbox.getAttribute('data-testid')}\"]` ||\n `input[type=\"checkbox\"][name=\"${consentCheckbox.name}\"]`,\n label: consentCheckbox.parentElement?.innerText,\n required: true\n });\n }\n\n // === CHAMPS DYNAMIQUES (questions sp\u00e9cifiques) ===\n \n // Tous les inputs number\n modal.querySelectorAll('input[type=\"number\"]').forEach(input => {\n // Ignorer les champs statiques\n if (!staticFields.some(f => input.name?.includes(f))) {\n const label = input.labels?.[0]?.innerText || \n input.closest('label')?.innerText ||\n input.previousElementSibling?.innerText ||\n input.placeholder;\n \n fields.dynamic.push({\n type: 'number',\n name: input.name || input.id,\n selector: `input[name=\"${input.name}\"]`,\n label: label,\n placeholder: input.placeholder,\n required: input.required\n });\n }\n });\n\n // Tous les groupes de radio buttons\n const radioGroups = {};\n modal.querySelectorAll('input[type=\"radio\"]').forEach(radio => {\n const groupName = radio.name;\n if (!radioGroups[groupName]) {\n const questionLabel = radio.closest('fieldset')?.querySelector('legend')?.innerText ||\n radio.closest('[role=\"group\"]')?.querySelector('label')?.innerText ||\n radio.parentElement?.parentElement?.querySelector('label')?.innerText;\n \n radioGroups[groupName] = {\n type: 'radio',\n name: groupName,\n question: questionLabel,\n options: []\n };\n }\n \n radioGroups[groupName].options.push({\n value: radio.value,\n label: radio.parentElement?.innerText?.trim() || radio.value,\n selector: `input[type=\"radio\"][name=\"${groupName}\"][value=\"${radio.value}\"]`\n });\n });\n \n Object.values(radioGroups).forEach(group => fields.dynamic.push(group));\n\n // Toutes les textareas (sauf cover letter d\u00e9j\u00e0 trait\u00e9e)\n modal.querySelectorAll('textarea').forEach(textarea => {\n if (!textarea.name?.includes('cover') && \n textarea.getAttribute('data-testid') !== 'apply-form-field-cover_letter') {\n const label = textarea.labels?.[0]?.innerText ||\n textarea.closest('label')?.innerText ||\n textarea.previousElementSibling?.innerText ||\n textarea.placeholder;\n \n fields.dynamic.push({\n type: 'textarea',\n name: textarea.name || textarea.id,\n selector: `textarea[name=\"${textarea.name}\"]`,\n label: label,\n placeholder: textarea.placeholder,\n required: textarea.required\n });\n }\n });\n\n return {\n fields: fields,\n text: modal.innerText.substring(0, 3000)\n };\n});\n\n// ===== \u00c9TAPE 5 : R\u00c9CUP\u00c9RER LES COOKIES =====\nconst cookies = await $page.cookies();\n\nreturn [{\n json: {\n status: 'form_extracted',\n jobId: jobId,\n jobLink: jobLink,\n formFields: formData.fields,\n formText: formData.text,\n cookies: cookies\n }\n}];\n\n",
"options": {
"browserWSEndpoint": "ws://browserless:3000",
"timeout": 50000
}
},
"type": "@crunchy-bytes/n8n-nodes-puppeteer.puppeteer",
"typeVersion": 1,
"position": [
128,
-448
],
"id": "02f148fa-431c-4e35-a4fb-9208ec077270",
"name": "Puppeteer Connexion3",
"disabled": true
},
{
"parameters": {
"operation": "runCustomScript",
"scriptCode": "// R\u00e9cup\u00e9rer cookies et infos\nconst cookies = $json.cookies;\nconst jobLink = $json.jobLink;\nconst jobId = $json.jobId;\n\n// Restaurer cookies\nawait $page.setCookie(...cookies);\n\n// Aller sur l'offre\nawait $page.goto(jobLink, {\n waitUntil: 'domcontentloaded',\n timeout: 30000\n});\n\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\n// Cliquer sur Postuler\nawait $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"job_bottom-button-apply\"]');\n if (btn) btn.click();\n});\n\nawait $page.waitForSelector('[data-testid=\"apply-form-modal\"]', { timeout: 10000 });\nawait new Promise(resolve => setTimeout(resolve, 2000));\n\n// ===== OUVRIR LES DROPDOWNS =====\nconsole.log('Expansion des dropdowns...');\n\nawait $page.evaluate(() => {\n const modal = document.querySelector('[data-testid=\"apply-form-modal\"]');\n if (!modal) return;\n \n // 1. aria-expanded=\"false\" (sauf boutons de fermeture)\n modal.querySelectorAll('[aria-expanded=\"false\"]').forEach(el => {\n const ariaLabel = el.getAttribute('aria-label') || '';\n const text = el.innerText || '';\n \n if (ariaLabel.toLowerCase().includes('close') || \n ariaLabel.toLowerCase().includes('fermer') ||\n text.trim() === '\u00d7' || text.trim() === 'X') {\n return;\n }\n \n try { el.click(); } catch(e) {}\n });\n \n // 2. Dropdowns WTTJ (combobox pattern)\n const comboboxes = modal.querySelectorAll('[role=\"combobox\"][aria-haspopup=\"listbox\"]');\n \n comboboxes.forEach(combo => {\n const arrowBtn = combo.querySelector('button[data-testid*=\"-arrow-icon\"]');\n \n if (arrowBtn && combo.getAttribute('aria-expanded') === 'false') {\n try {\n arrowBtn.click();\n } catch(e) {}\n }\n });\n});\n\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\n// V\u00e9rifier que le modal est toujours l\u00e0\nconst modalStillExists = await $page.evaluate(() => {\n return !!document.querySelector('[data-testid=\"apply-form-modal\"]');\n});\n\nif (!modalStillExists) {\n throw new Error('\u274c Le modal a \u00e9t\u00e9 ferm\u00e9 pendant l\\'expansion !');\n}\n\nconsole.log('Expansion termin\u00e9e');\n\n// ===== EXTRACTION =====\nconst formData = await $page.evaluate(() => {\n const modal = document.querySelector('[data-testid=\"apply-form-modal\"]');\n if (!modal) return { fields: { static: [], semiDynamic: [], dynamic: [] }, text: '' };\n \n const fields = { static: [], semiDynamic: [], dynamic: [] };\n const processed = new Set();\n \n // ===== HELPERS =====\n function findLabel(element) {\n if (element.labels && element.labels.length > 0) {\n return element.labels[0].innerText.trim();\n }\n \n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel;\n \n const labelId = element.getAttribute('aria-labelledby');\n if (labelId) {\n const labelEl = document.getElementById(labelId);\n if (labelEl) return labelEl.innerText.trim();\n }\n \n let prev = element.previousElementSibling;\n while (prev) {\n const text = prev.innerText?.trim();\n if (text && text.length > 0 && text.length < 100) {\n return text;\n }\n prev = prev.previousElementSibling;\n }\n \n const parent = element.closest('[data-testid*=\"field\"], fieldset, div');\n if (parent) {\n const label = parent.querySelector('label, legend');\n if (label) return label.innerText.trim();\n }\n \n return element.placeholder || element.name || 'Unknown';\n }\n \n function isStatic(name, label) {\n const staticKeywords = ['firstname', 'first_name', 'lastname', 'last_name', 'email', 'phone', 'location', 'website', 'linkedin', 'twitter', 'x.com'];\n const nameL = (name || '').toLowerCase();\n const labelL = (label || '').toLowerCase();\n return staticKeywords.some(k => nameL.includes(k) || labelL.includes(k));\n }\n \n // ===== SEMI-DYNAMIC =====\n \n // CV\n const cvInput = modal.querySelector('input[type=\"file\"]:not([name*=\"avatar\"])');\n if (cvInput) {\n const label = findLabel(cvInput);\n if (!label.toLowerCase().includes('avatar') && !label.toLowerCase().includes('photo')) {\n fields.semiDynamic.push({\n type: 'file',\n name: 'cv',\n selector: 'input[type=\"file\"]:not([name*=\"avatar\"])',\n label: label,\n accept: cvInput.accept,\n required: cvInput.required || true\n });\n processed.add(cvInput.name || 'cv');\n }\n }\n \n // Cover Letter\n const coverTextarea = modal.querySelector('[data-testid=\"apply-form-field-cover_letter\"], textarea[name*=\"cover\"], textarea[placeholder*=\"why\"]');\n if (coverTextarea) {\n fields.semiDynamic.push({\n type: 'textarea',\n name: coverTextarea.name || 'cover_letter',\n selector: coverTextarea.getAttribute('data-testid') ? \n `[data-testid=\"${coverTextarea.getAttribute('data-testid')}\"]` :\n `textarea[name=\"${coverTextarea.name}\"]`,\n label: findLabel(coverTextarea),\n placeholder: coverTextarea.placeholder,\n required: coverTextarea.required || false\n });\n processed.add(coverTextarea.name || 'cover_letter');\n }\n \n // Consentement\n const consentCheckbox = modal.querySelector('[data-testid=\"apply-form-consent\"], input[type=\"checkbox\"][required]');\n if (consentCheckbox) {\n const label = findLabel(consentCheckbox);\n if (label.toLowerCase().includes('agree') || label.toLowerCase().includes('accept') || label.toLowerCase().includes('consent')) {\n fields.semiDynamic.push({\n type: 'checkbox',\n name: 'consent',\n selector: consentCheckbox.getAttribute('data-testid') ?\n `[data-testid=\"${consentCheckbox.getAttribute('data-testid')}\"]` :\n `input[type=\"checkbox\"][required]`,\n label: label,\n required: true\n });\n processed.add(consentCheckbox.name || 'consent');\n }\n }\n \n // ===== DYNAMIC =====\n \n // Inputs\n modal.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"radio\"]):not([type=\"checkbox\"]):not([type=\"file\"])').forEach(input => {\n const key = input.name || input.id || Math.random().toString();\n if (processed.has(key)) return;\n \n const label = findLabel(input);\n \n if (isStatic(input.name, label)) {\n fields.static.push({ name: input.name || input.id, value: input.value, label: label });\n return;\n }\n \n processed.add(key);\n fields.dynamic.push({\n type: 'input',\n inputType: input.type,\n name: input.name || input.id,\n selector: input.name ? `input[name=\"${input.name}\"]` : `#${input.id}`,\n label: label,\n placeholder: input.placeholder,\n required: input.required,\n value: input.value\n });\n });\n \n // Textareas\n modal.querySelectorAll('textarea').forEach(textarea => {\n const key = textarea.name || textarea.id || Math.random().toString();\n if (processed.has(key)) return;\n processed.add(key);\n \n fields.dynamic.push({\n type: 'textarea',\n name: textarea.name || textarea.id,\n selector: textarea.name ? `textarea[name=\"${textarea.name}\"]` : `#${textarea.id}`,\n label: findLabel(textarea),\n placeholder: textarea.placeholder,\n required: textarea.required\n });\n });\n \n // SELECT natifs\n modal.querySelectorAll('select').forEach(select => {\n const key = select.name || select.id || Math.random().toString();\n if (processed.has(key)) return;\n processed.add(key);\n \n const options = Array.from(select.options)\n .filter(opt => opt.value)\n .map(opt => ({ value: opt.value, label: opt.text.trim() }));\n \n fields.dynamic.push({\n type: 'select',\n name: select.name || select.id,\n selector: select.name ? `select[name=\"${select.name}\"]` : `#${select.id}`,\n label: findLabel(select),\n options: options,\n required: select.required\n });\n });\n \n // Radio buttons\n const radioGroups = {};\n modal.querySelectorAll('input[type=\"radio\"]').forEach(radio => {\n const groupName = radio.name;\n if (!radioGroups[groupName]) {\n radioGroups[groupName] = {\n type: 'radio',\n name: groupName,\n label: findLabel(radio),\n options: []\n };\n }\n \n radioGroups[groupName].options.push({\n value: radio.value,\n label: radio.parentElement?.innerText?.trim() || radio.value,\n selector: `input[type=\"radio\"][name=\"${groupName}\"][value=\"${radio.value}\"]`\n });\n });\n \n Object.values(radioGroups).forEach(group => {\n if (!processed.has(group.name)) {\n processed.add(group.name);\n fields.dynamic.push(group);\n }\n });\n \n // ===== DROPDOWNS WTTJ (combobox pattern) =====\n modal.querySelectorAll('[role=\"combobox\"][aria-haspopup=\"listbox\"]').forEach(combobox => {\n const field = combobox.querySelector('[data-testid*=\"-DROPDOWN\"]');\n if (!field) return;\n \n const name = field.getAttribute('name');\n const testId = field.getAttribute('data-testid');\n \n if (!name || processed.has(name)) return;\n processed.add(name);\n \n const labelId = combobox.getAttribute('aria-labelledby');\n const labelEl = labelId ? document.getElementById(labelId) : null;\n const label = labelEl ? labelEl.innerText.trim() : field.getAttribute('placeholder') || 'Unknown';\n \n // Chercher le listbox\n let listbox = document.querySelector(`[role=\"listbox\"][aria-labelledby=\"${labelId}\"]`);\n \n const options = [];\n if (listbox) {\n listbox.querySelectorAll('[role=\"option\"]').forEach(opt => {\n const value = opt.getAttribute('data-value') || opt.innerText.trim();\n const optLabel = opt.innerText.trim();\n if (value && optLabel) {\n options.push({ value, label: optLabel });\n }\n });\n }\n \n fields.dynamic.push({\n type: 'dropdown_wttj',\n name: name,\n testId: testId,\n selector: `[data-testid=\"${testId}\"]`,\n buttonSelector: `button[data-testid=\"${testId.replace('-DROPDOWN', '-DROPDOWN-arrow-icon')}\"]`,\n label: label,\n placeholder: field.getAttribute('placeholder'),\n options: options,\n required: field.hasAttribute('required')\n });\n });\n \n // Checkboxes\n modal.querySelectorAll('input[type=\"checkbox\"]').forEach(checkbox => {\n const key = checkbox.name || checkbox.id || Math.random().toString();\n if (processed.has(key)) return;\n processed.add(key);\n \n fields.dynamic.push({\n type: 'checkbox',\n name: checkbox.name || checkbox.id,\n selector: checkbox.getAttribute('data-testid') ?\n `[data-testid=\"${checkbox.getAttribute('data-testid')}\"]` :\n `input[type=\"checkbox\"][name=\"${checkbox.name}\"]`,\n label: findLabel(checkbox),\n required: checkbox.required\n });\n });\n \n return {\n fields: fields,\n text: modal.innerText.substring(0, 3000)\n };\n});\n\nreturn [{\n json: {\n status: 'form_analyzed',\n jobId: jobId,\n jobLink: jobLink,\n formFields: formData.fields,\n formText: formData.text,\n // cookies: cookies\n }\n}];",
"options": {
"browserWSEndpoint": "ws://browserless:3000",
"timeout": 50000
}
},
"type": "@crunchy-bytes/n8n-nodes-puppeteer.puppeteer",
"typeVersion": 1,
"position": [
1424,
-112
],
"id": "3f6c9c12-a515-4694-8225-69aed8108d94",
"name": "Puppeteer Cookie Injection"
},
{
"parameters": {
"operation": "runCustomScript",
"scriptCode": "// ===== CONFIGURATION DU LOGGER POUR n8n =====\nlet debugLogs = [];\nconst logger = (msg) => {\n const entry = `${new Date().toLocaleTimeString()} - ${msg}`;\n console.log(entry); \n debugLogs.push(entry);\n};\n\n// ===== SCREENSHOT DE VERIFICATION =====\nawait $page.setViewport({ width: 1400, height: 2500 });\nawait $page.addStyleTag({ content: `\n [data-testid=\"modals\"] { align-items: flex-start !important; }\n [data-testid=\"apply-form-modal\"] { height: auto !important; position: relative !important; }\n`});\n\n// ===== R\u00c9CUP\u00c9RATION DES DONN\u00c9ES =====\nconst cookies = $('Puppeteer Connexion + Recup\u00e9ration Cookie').item.json.cookies;\nconst { jobLink, jobId, cv_filename: cvFilename, cover_letter: coverLetter, dynamic_fields: dynamicFields, consent } = $items(\"Nettoyage donn\u00e9e JSON\")[0].json;\n\nlogger(`\ud83c\udfaf D\u00e9but candidature - Offre: ${jobId}`);\n\n// ===== NAVIGATION NATURELLE =====\nlogger('\ud83c\udf10 Simulation navigation r\u00e9elle...');\n// 1. On va sur la home d'abord\nawait $page.goto('https://www.welcometothejungle.com/fr', { waitUntil: 'networkidle2' });\nawait new Promise(r => setTimeout(r, 2000));\n\n// 2. On va sur l'offre (le Referer sera maintenant correct)\nawait $page.goto(jobLink, {\n waitUntil: 'networkidle2', // On attend que TOUS les scripts (tracking inclus) soient charg\u00e9s\n timeout: 25000\n});\nlogger('\u2705 Page offre charg\u00e9e avec historique');\n\n// 3. Petit mouvement de souris sur la page de l'offre avant de cliquer sur postuler\nawait $page.mouse.move(100, 100);\nawait $page.mouse.move(200, 300, { steps: 5 });\n\n// ===== RESTAURER SESSION =====\nawait $page.setCookie(...cookies);\n\n// ===== INTERCEPTION DES ERREURS SERVEUR =====\nlet serverErrorDetail = \"Aucune erreur d\u00e9tect\u00e9e\";\n\n$page.on('response', async (response) => {\n const url = response.url();\n // On cible l'appel API qui envoie la candidature\n if (url.includes('/applications') && response.status() >= 400) {\n try {\n const errorJson = await response.json();\n serverErrorDetail = `Code ${response.status()}: ${JSON.stringify(errorJson)}`;\n logger(`\u274c ERREUR SERVEUR D\u00c9TECT\u00c9E : ${serverErrorDetail}`);\n } catch (e) {\n serverErrorDetail = `Erreur ${response.status()} (pas de JSON)`;\n }\n }\n});\n\n// ===== NAVIGATION =====\nawait $page.goto(jobLink, { waitUntil: 'domcontentloaded', timeout: 26000 });\nawait new Promise(r => setTimeout(r, 3000));\n\n// ===== CLIQUER SUR POSTULER =====\nawait $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"job_bottom-button-apply\"]');\n if (btn) btn.click();\n});\n\nawait $page.waitForSelector('[data-testid=\"apply-form-modal\"]', { timeout: 10000 });\nlogger('\u2705 Modal ouvert');\n\n// Juste apr\u00e8s : await $page.waitForSelector('[data-testid=\"apply-form-modal\"]')\nlogger('\u2705 Modal ouvert');\n\n// // ===== UPLOAD DIRECT (CHEMIN UNIFI\u00c9) =====\n// const cvPath = `/home/node/.n8n-files/cv/${cvFilename}`;\n// logger(`\ud83d\udcc2 Chemin unifi\u00e9 utilis\u00e9 : ${cvPath}`);\n\n// try {\n// const inputHandle = await $page.waitForSelector('input#resume', { timeout: 10000 });\n \n// // Le navigateur va chercher le fichier \u00e0 l'int\u00e9rieur de SON propre conteneur\n// await inputHandle.uploadFile(cvPath);\n \n// await $page.evaluate(() => {\n// const input = document.querySelector('input#resume');\n// if (input) {\n// input.dispatchEvent(new Event('change', { bubbles: true }));\n// input.dispatchEvent(new Event('input', { bubbles: true }));\n// }\n// });\n\n// const checkSize = await $page.evaluate(() => {\n// return document.querySelector('input#resume')?.files[0]?.size || 0;\n// });\n\n// if (checkSize > 0) {\n// logger(`\u2705 SUCC\u00c8S : Fichier trouv\u00e9 par Browserless (${checkSize} octets)`);\n// } else {\n// logger(`\u274c \u00c9CHEC : Taille 0. Browserless ne voit toujours pas le fichier.`);\n// }\n// } catch (e) {\n// logger(`\u274c Erreur : ${e.message}`);\n// }\n\n// // ==== INJECTION DU CV REVISIT\u00c9E ====\n// logger('\ud83d\udce4 Extraction du binaire...');\n\n// const nodeOutput = $items(\"Read/Write Files from Disk\")[0];\n// let rawBase64 = null;\n\n// // n8n peut stocker le base64 soit dans .data, soit dans .data.data\n// if (nodeOutput.binary && nodeOutput.binary.data) {\n// rawBase64 = typeof nodeOutput.binary.data === 'object' \n// ? nodeOutput.binary.data.data \n// : nodeOutput.binary.data;\n// }\n\n// if (rawBase64 && rawBase64.length > 1000) { // On v\u00e9rifie qu'on a bien des milliers de caract\u00e8res\n// logger(`\u2705 Base64 valide d\u00e9tect\u00e9 (${rawBase64.length} caract\u00e8res)`);\n \n// await $page.evaluate(async (b64, name) => {\n// // 1. Reconstitution du fichier en m\u00e9moire vive\n// const binStr = atob(b64);\n// const bytes = new Uint8Array(binStr.length);\n// for (let i = 0; i < binStr.length; i++) {\n// bytes[i] = binStr.charCodeAt(i);\n// }\n// const file = new File([bytes], name, { type: 'application/pdf' });\n\n// // 2. Injection dans l'input\n// const input = document.querySelector('input[type=\"file\"]:not([name*=\"avatar\"])');\n// if (input) {\n// const dt = new DataTransfer();\n// dt.items.add(file);\n// input.files = dt.files;\n\n// // 3. R\u00e9veil de React (WTTJ)\n// input.dispatchEvent(new Event('input', { bubbles: true }));\n// input.dispatchEvent(new Event('change', { bubbles: true }));\n// return true;\n// }\n// }, rawBase64, nodeOutput.binary.data.fileName || \"CV.pdf\");\n\n// // V\u00e9rification r\u00e9elle du poids\n// const checkSize = await $page.evaluate(() => {\n// return document.querySelector('input[type=\"file\"]:not([name*=\"avatar\"])')?.files[0]?.size || 0;\n// });\n// logger(`\ud83d\udccf Poids inject\u00e9 en m\u00e9moire : ${checkSize} octets`);\n// } else {\n// logger(`\u274c ERREUR : Binaire corrompu (Length: ${rawBase64?.length || 0})`);\n// }\n\n// ===== REMPLIR LETTRE DE MOTIVATION (OPTIONNEL) =====\nconst lmSelector = '[data-testid=\"apply-form-field-cover_letter\"]';\n\n// On v\u00e9rifie si l'\u00e9l\u00e9ment existe sans attendre 5 secondes inutilement\nconst lmElement = await $page.$(lmSelector);\n\nif (lmElement) {\n logger('\ud83d\udcdd Champ lettre de motivation d\u00e9tect\u00e9, remplissage en cours...');\n \n // Vider le champ proprement via evaluate\n await $page.evaluate((sel) => {\n const el = document.querySelector(sel);\n if (el) {\n el.value = '';\n el.dispatchEvent(new Event('input', { bubbles: true }));\n }\n }, lmSelector);\n\n // Taper la lettre\n await $page.type(lmSelector, coverLetter, { delay: 1 });\n\n // Blur pour simuler la sortie du champ et valider la saisie\n await $page.evaluate((sel) => {\n const el = document.querySelector(sel);\n if (el) el.blur();\n }, lmSelector);\n\n logger('\u2705 Lettre de motivation remplie');\n} else {\n // Si le champ n'existe pas, on passe simplement \u00e0 la suite sans erreur\n logger('\u2139\ufe0f Pas de champ lettre de motivation sur cette offre, \u00e9tape saut\u00e9e.');\n}\n\n// ===== REMPLIR LES CHAMPS DYNAMIQUES =====\nfor (const field of dynamicFields) {\n logger(`\ud83d\udcdd Champ: ${field.label}`);\n \n try {\n if (field.type === 'input' || field.type === 'textarea') {\n await $page.waitForSelector(field.selector, { timeout: 3000 });\n await $page.focus(field.selector);\n \n // On vide via JS pour la rapidit\u00e9\n await $page.evaluate((sel) => { document.querySelector(sel).value = ''; }, field.selector);\n \n // On tape avec un d\u00e9lai tr\u00e8s court pour \u00e9viter le timeout\n await $page.type(field.selector, String(field.value), { delay: 5 });\n await $page.keyboard.press('Tab');\n } \n else if (field.type === 'dropdown_wttj') {\n await $page.click(field.buttonSelector);\n await new Promise(r => setTimeout(r, 800));\n await $page.evaluate((f) => {\n const options = Array.from(document.querySelectorAll('[role=\"option\"]'));\n const target = options.find(opt => opt.innerText.trim() === f.value || opt.getAttribute('data-value') === f.value);\n if (target) target.click();\n }, field);\n }\n else if (field.type === 'radio' || (field.type === 'checkbox' && field.name !== 'consent')) {\n await $page.evaluate((f) => {\n const el = document.querySelector(f.selector);\n if (el) el.click();\n }, field);\n }\n } catch (e) {\n logger(`\u26a0\ufe0f Erreur sur ${field.label}: ${e.message}`);\n }\n}\n\n// ===== CONSENTEMENT =====\nawait $page.evaluate((sel) => {\n const cb = document.querySelector(sel);\n if (cb && !cb.checked) {\n cb.focus();\n cb.click();\n }\n}, consent.selector);\nlogger('\u2705 Consentement coch\u00e9');\n\n// 1. ATTENTE DE S\u00c9CURIT\u00c9 (Laisser les scripts de validation respirer)\nlogger('\u23f3 Attente de validation des champs...');\nawait new Promise(r => setTimeout(r, 3000));\n\n// 2. SIMULER UN CLIC DANS LE VIDE (Pour d\u00e9clencher les \u00e9v\u00e9nements 'blur')\nawait $page.mouse.click(10, 10); \n\n// 3. SCROLL PROGRESSIF JUSQU'AU BOUTON\nlogger('\ud83d\uddb1\ufe0f Scroll vers le bouton submit...');\nawait $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"apply-form-submit\"]');\n if (btn) btn.scrollIntoView({ behavior: 'smooth', block: 'center' });\n});\nawait new Promise(r => setTimeout(r, 2000));\n\n// 4. V\u00c9RIFICATION DU STATUT R\u00c9EL DU BOUTON\nconst buttonInfo = await $page.evaluate((sel) => {\n const btn = document.querySelector(sel);\n if (!btn) return \"NOT_FOUND\";\n return {\n disabled: btn.disabled,\n visible: btn.offsetParent !== null,\n text: btn.innerText\n };\n}, '[data-testid=\"apply-form-submit\"]');\n\nlogger(`\ud83d\udd0d Infos Bouton : Disabled=${buttonInfo.disabled}, Visible=${buttonInfo.visible}`);\n\n// 5. CLIC PHYSIQUE AVEC MOUVEMENT\nif (buttonInfo.disabled === false) {\n const submitBtn = await $page.$('[data-testid=\"apply-form-submit\"]');\n const box = await submitBtn.boundingBox();\n \n // On d\u00e9place la souris progressivement vers le bouton (tr\u00e8s important pour les anti-bots)\n await $page.mouse.move(box.x, box.y);\n await new Promise(r => setTimeout(r, 200));\n await $page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });\n\n logger('\u23f3 Pause humaine pr\u00e9-envoi...');\n await new Promise(r => setTimeout(r, 4000)); // L'humain relit souvent avant de cliquer\n \n await $page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);\n logger('\ud83d\ude80 Submit cliqu\u00e9 avec trajectoire souris');\n} else {\n logger('\u274c Abandon : Le bouton est toujours d\u00e9sactiv\u00e9 dans le DOM');\n}\n\n// 6. ATTENTE DU RETOUR SERVEUR (Indispensable pour voir l'erreur rouge ou le succ\u00e8s)\n// await new Promise(r => setTimeout(r, 10000));\n\n// logger('\u23f8\ufe0f DEBUT PAUSE MANUELLE (5 min)');\n\n// // On d\u00e9finit un temps de fin (Maintenant + 5 minutes)\n// const endTime = Date.now() + (5 * 60 * 1000);\n\n// while (Date.now() < endTime) {\n// // On fait une petite action inoffensive pour garder la connexion active\n// await $page.title(); \n// // On attend 5 secondes avant de recommencer le \"ping\"\n// await new Promise(r => setTimeout(r, 5000));\n// logger(`\u23f3 Toujours en pause... encore ${(Math.round((endTime - Date.now())/1000))}s`);\n// }\n\n// logger('\u23f0 FIN PAUSE MANUELLE');\n\nawait new Promise(r => setTimeout(r, 2000));\n\nawait $page.screenshot({ path: '/tmp/fill-06-after-submit.png' });\n\nreturn [{\n json: {\n status: 'finished',\n jobId,\n serverError: serverErrorDetail, // <--- C'EST CETTE LIGNE QUI NOUS DONNERA LA R\u00c9PONSE\n logs: debugLogs\n }\n}];",
"query": "=",
"options": {
"browserWSEndpoint": "ws://browserless:3000",
"timeout": 40000
}
},
"type": "@crunchy-bytes/n8n-nodes-puppeteer.puppeteer",
"typeVersion": 1,
"position": [
2448,
-112
],
"id": "4b445e82-2356-4966-99f7-3f0a008e0635",
"name": "Puppeteer Application",
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "runCustomScript",
"scriptCode": "// ===== R\u00c9CUP\u00c9RATION DES DONN\u00c9ES =====\nconst cookies = $('Puppeteer Connexion').item.json.cookies;\nconst jobLink = $json.jobLink;\nconst jobId = $json.jobId;\nconst cvFilename = $json.cv_filename;\nconst coverLetter = $json.cover_letter;\nconst dynamicFields = $json.dynamic_fields;\nconst consent = $json.consent;\n\n// Activer interception\nconst networkLogs = { requests: [], responses: [] };\n\nawait $page.setRequestInterception(true);\n\n$page.on('request', req => {\n const url = req.url();\n if (url.includes('apply') || url.includes('submit') || url.includes('candidate') || url.includes('graphql')) {\n networkLogs.requests.push({\n url: url,\n method: req.method(),\n postData: req.postData()?.substring(0, 500)\n });\n }\n req.continue();\n});\n\n$page.on('response', async res => {\n const url = res.url();\n if (url.includes('apply') || url.includes('submit') || url.includes('candidate') || url.includes('graphql')) {\n try {\n const text = await res.text();\n networkLogs.responses.push({\n url: url,\n status: res.status(),\n body: text.substring(0, 1000)\n });\n } catch(e) {}\n }\n});\n\n// Restaurer cookies + navigation\nawait $page.setCookie(...cookies);\nawait $page.goto(jobLink, { waitUntil: 'domcontentloaded', timeout: 30000 });\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\n// Cliquer Postuler\nawait $page.evaluate(() => {\n document.querySelector('[data-testid=\"job_bottom-button-apply\"]')?.click();\n});\nawait $page.waitForSelector('[data-testid=\"apply-form-modal\"]', { timeout: 10000 });\nawait new Promise(resolve => setTimeout(resolve, 2000));\n\n// Remplir rapidement (version simplifi\u00e9e)\nconst fileInput = await $page.$('input[type=\"file\"]:not([name*=\"avatar\"])');\nawait fileInput.uploadFile('/home/node/cv/' + cvFilename);\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\nawait $page.evaluate((lm) => {\n document.querySelector('[data-testid=\"apply-form-field-cover_letter\"]').value = lm;\n}, coverLetter);\n\n// Remplir champs dynamiques (version rapide)\nfor (const field of dynamicFields) {\n if (field.type === 'input') {\n await $page.evaluate((f) => {\n document.querySelector(f.selector).value = f.value;\n }, field);\n } else if (field.type === 'dropdown_wttj') {\n await $page.evaluate((f) => {\n document.querySelector(f.buttonSelector)?.click();\n }, field);\n await new Promise(resolve => setTimeout(resolve, 1000));\n await $page.evaluate((f) => {\n const option = Array.from(document.querySelectorAll('[role=\"option\"]'))\n .find(o => o.innerText.trim() === f.value);\n option?.click();\n }, field);\n await new Promise(resolve => setTimeout(resolve, 500));\n }\n}\n\n// Consentement\nawait $page.evaluate((sel) => {\n document.querySelector(sel)?.click();\n}, consent.selector);\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\n// \u00c9tat PRE-submit\nconst preSubmit = await $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"apply-form-submit\"]');\n return {\n buttonExists: !!btn,\n buttonDisabled: btn?.disabled,\n buttonText: btn?.innerText\n };\n});\n\n// Screenshot\nawait $page.screenshot({ path: '/tmp/diagnostic-before-submit.png', fullPage: true });\n\n// CLIQUER SUBMIT\nawait $page.evaluate(() => {\n document.querySelector('[data-testid=\"apply-form-submit\"]')?.click();\n});\n\n// Attendre\nawait new Promise(resolve => setTimeout(resolve, 5000));\n\n// \u00c9tat POST-submit\nconst postSubmit = await $page.evaluate(() => {\n const errors = Array.from(document.querySelectorAll('[role=\"alert\"]'))\n .filter(el => el.offsetParent !== null)\n .map(el => el.innerText);\n \n return {\n errorMessages: errors,\n modalOpen: !!document.querySelector('[data-testid=\"apply-form-modal\"]')?.offsetParent,\n url: window.location.href\n };\n});\n\n// Screenshot apr\u00e8s\nawait $page.screenshot({ path: '/tmp/diagnostic-after-submit.png', fullPage: true });\n\nreturn [{\n json: {\n status: 'diagnostic_complete',\n preSubmit: preSubmit,\n postSubmit: postSubmit,\n networkLogs: networkLogs,\n jobId: jobId\n }\n}];",
"options": {
"browserWSEndpoint": "ws://browserless:3000",
"timeout": 30000
}
},
"type": "@crunchy-bytes/n8n-nodes-puppeteer.puppeteer",
"typeVersion": 1,
"position": [
1808,
256
],
"id": "43d0c096-2a8f-41a0-bb3d-3b34e2454629",
"name": "Puppeteer Debugging",
"disabled": true
},
{
"parameters": {
"operation": "runCustomScript",
"scriptCode": "// ===== CONFIGURATION =====\nconst email = 'YOUR_WTTJ_EMAIL';\nconst password = 'YOUR_WTTJ_PASSWORD';\n\n// R\u00e9cup\u00e9rer les infos de l'offre directement depuis Postgres\nconst jobId = $('Execute a SQL query').item.json.id;\nconst jobLink = $('Execute a SQL query').item.json.link;\n\nconsole.log(`\ud83c\udfaf Offre ID ${jobId} : ${jobLink}`);\n\n// ===============================\n// \u00c9TAPE 1 : CONNEXION \u00c0 WTTJ\n// ===============================\nconsole.log('\ud83d\udd10 Connexion \u00e0 WTTJ...');\n\nawait $page.goto('https://www.welcometothejungle.com/fr/signin', {\n waitUntil: 'networkidle2',\n timeout: 30000\n});\n\nawait new Promise(resolve => setTimeout(resolve, 3000));\n\nawait $page.screenshot({ path: '/tmp/01-login-page.png', fullPage: true });\n\n// ===== FERMER POPUP R\u00c9GION =====\nawait $page.evaluate(() => {\n const allButtons = Array.from(document.querySelectorAll('button, [role=\"button\"]'));\n \n const frBtn = allButtons.find(b => {\n const text = (b.innerText || '').toLowerCase();\n return text.includes('rester sur le site fran\u00e7ais');\n });\n \n if (frBtn) frBtn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\n// ===== ACCEPTER COOKIES =====\n// Utilise l'ID exact du bouton\nawait $page.evaluate(() => {\n const btn = document.querySelector('#axeptio_btn_acceptAll');\n if (btn) btn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\nawait $page.screenshot({ path: '/tmp/01b-popups-closed.png', fullPage: true });\n\n// ===== REMPLIR EMAIL =====\nconst emailFilled = await $page.evaluate((emailValue) => {\n const inputs = Array.from(document.querySelectorAll('input'));\n const emailInput = inputs.find(i => \n i.type === 'email' || \n i.name === 'email' ||\n (i.placeholder || '').toLowerCase().includes('adresse email')\n );\n if (emailInput) {\n emailInput.focus();\n emailInput.value = emailValue;\n emailInput.dispatchEvent(new Event('input', { bubbles: true }));\n emailInput.dispatchEvent(new Event('change', { bubbles: true }));\n emailInput.blur();\n return true;\n }\n return false;\n}, email);\n\nif (!emailFilled) throw new Error('\u274c Champ email introuvable');\nconsole.log('\u2705 Email saisi');\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== REMPLIR MOT DE PASSE =====\nconst passwordFilled = await $page.evaluate((pwdValue) => {\n const inputs = Array.from(document.querySelectorAll('input'));\n const pwdInput = inputs.find(i => \n i.type === 'password' || i.name === 'password'\n );\n if (pwdInput) {\n pwdInput.focus();\n pwdInput.value = pwdValue;\n pwdInput.dispatchEvent(new Event('input', { bubbles: true }));\n pwdInput.dispatchEvent(new Event('change', { bubbles: true }));\n pwdInput.blur();\n return true;\n }\n return false;\n}, password);\n\nif (!passwordFilled) throw new Error('\u274c Champ mot de passe introuvable');\nconsole.log('\u2705 Mot de passe saisi');\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== COCHER \"ME GARDER CONNECT\u00c9\" =====\nawait $page.evaluate(() => {\n const checkboxes = Array.from(document.querySelectorAll('input[type=\"checkbox\"]'));\n const rememberCheckbox = checkboxes.find(c => {\n const label = c.parentElement?.innerText || '';\n return label.toLowerCase().includes('me garder');\n });\n if (rememberCheckbox && !rememberCheckbox.checked) rememberCheckbox.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 500));\n\n// ===== AFFICHER MOT DE PASSE =====\nawait $page.evaluate(() => {\n const eyeBtn = document.querySelector('[data-testid=\"login-field-password-action\"]');\n if (eyeBtn) eyeBtn.click();\n});\n\nawait new Promise(resolve => setTimeout(resolve, 500));\n\nawait $page.screenshot({ path: '/tmp/02-form-filled.png', fullPage: true });\n\n// ===== CLIQUER SUR \"SE CONNECTER\" =====\nconst clicked = await $page.evaluate(() => {\n const btn = document.querySelector('[data-testid=\"login-button-submit\"]');\n if (!btn || btn.disabled) return false;\n btn.click();\n return true;\n});\n\nif (!clicked) throw new Error('\u274c Bouton Se connecter introuvable');\nconsole.log('\u2705 Connexion en cours...');\n\nawait new Promise(resolve => setTimeout(resolve, 7000));\n\nawait $page.screenshot({ path: '/tmp/03-after-login.png', fullPage: true });\n\n// ===== V\u00c9RIFIER CONNEXION =====\nconst loginUrl = $page.url();\nif (loginUrl.includes('signin') || loginUrl.includes('login')) {\n const errorMsg = await $page.evaluate(() => {\n const el = document.querySelector('[class*=\"error\"], [role=\"alert\"]');\n return el ? el
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.
postgrestelegramApivercelAiGatewayApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WTTJ job apply - Workflow2. Uses telegramTrigger, postgres, agent, lmChatVercelAiGateway. Event-driven trigger; 24 nodes.
Source: https://github.com/ArthSogh/autoapply-n8n-bot/blob/d2ddb4efbe69bf539e061e7e86ad32ab0eea8372/workflows/W2_Applicant.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This powerful workflow automates the evaluation of new digital tools, websites, or platforms with the goal of assessing their potential impact on your business. By leveraging Telegram for user input,
This workflow connects Amazon product lookups to Telegram using AI-enhanced scraping and automation. It lets users send a product name to a Telegram bot and instantly receive pricing, discount, and pr
This workflow is designed for entrepreneurs, sales teams, marketers, and agencies who want to automate lead discovery and build qualified business contact lists — without manual searching or copying d
Business owners, industry specialists, and AI developers building domain-specific search experiences. It generates custom search filters to boost technical authorities and block lead-gen aggregators,
Turn any Amazon India product URL into a fully-edited 10-second lifestyle video and auto-publish it to Instagram, Facebook, X (Twitter), LinkedIn, YouTube, and Threads — with platform-optimized captio