This workflow follows the HTTP Request → Postgres 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": "Empleabilidad Colombia - Employment Search Workflow",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "employment-search",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-trigger",
"name": "\ud83d\ude80 Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "// Normalizar y validar datos del webhook\nconst body = $input.first().json.body || $input.first().json;\n\nconst searchId = body.search_id;\nconst userId = body.user_id;\nconst profile = body.profile;\nconst criteria = body.criteria || {};\n\nif (!searchId || !userId || !profile) {\n throw new Error('Datos incompletos: se requiere search_id, user_id y profile');\n}\n\n// Normalizar perfil\nconst normalizedProfile = {\n user_id: userId,\n city: (profile.city || '').toLowerCase().trim(),\n available_to_relocate: Boolean(profile.available_to_relocate),\n relocation_cities: Array.isArray(profile.relocation_cities) ? profile.relocation_cities : [],\n work_modality: Array.isArray(profile.work_modality) ? profile.work_modality : ['presencial'],\n immediate_availability: Boolean(profile.immediate_availability !== false),\n desired_salary_min: parseInt(profile.desired_salary_min) || null,\n desired_salary_max: parseInt(profile.desired_salary_max) || null,\n education_level: profile.education_level || null,\n years_experience: parseInt(profile.years_experience) || 0,\n experience_sectors: Array.isArray(profile.experience_sectors) ? profile.experience_sectors : [],\n technical_skills: Array.isArray(profile.technical_skills) ? profile.technical_skills : [],\n soft_skills: Array.isArray(profile.soft_skills) ? profile.soft_skills : [],\n languages: Array.isArray(profile.languages) ? profile.languages : [],\n bio: profile.bio || ''\n};\n\nconsole.log(`[NORMALIZE] search_id=${searchId} user_id=${userId} skills=${normalizedProfile.technical_skills.length}`);\n\nreturn [{\n json: {\n search_id: searchId,\n user_id: userId,\n profile: normalizedProfile,\n criteria: {\n city: criteria.city || profile.city,\n work_modality: criteria.work_modality || profile.work_modality,\n min_salary: criteria.min_salary || profile.desired_salary_min,\n max_salary: criteria.max_salary || profile.desired_salary_max,\n keywords: criteria.keywords || '',\n categories: criteria.categories || []\n },\n timestamp: new Date().toISOString()\n }\n}];"
},
"id": "normalize-data",
"name": "\ud83d\udd27 Normalizar Datos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE searches SET status = 'processing', workflow_execution_id = '{{ $execution.id }}' WHERE id = '{{ $json.search_id }}' RETURNING id, status;",
"options": {}
},
"id": "update-search-processing",
"name": "\ud83d\udcdd Marcar como Procesando",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
680,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT j.id, j.title, j.company_name, j.company_logo_url, j.city, j.work_modality, j.contract_type, j.required_education, j.required_experience_years, j.required_skills, j.preferred_skills, j.salary_min, j.salary_max, j.description, j.category, j.tags FROM jobs j WHERE j.status = 'active' AND ({{ $('\ud83d\udd27 Normalizar Datos').item.json.criteria.city != '' }} = false OR j.city ILIKE '%{{ $('\ud83d\udd27 Normalizar Datos').item.json.criteria.city }}%' OR j.work_modality = 'remoto') LIMIT 50;",
"options": {}
},
"id": "fetch-jobs",
"name": "\ud83d\uddc4\ufe0f Consultar Vacantes",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
900,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Agrupar todas las vacantes en un solo objeto para enviar al scoring\nconst allItems = $input.all();\nconst normalizeNode = $('\ud83d\udd27 Normalizar Datos').first().json;\n\nif (!allItems || allItems.length === 0) {\n console.log('[AGGREGATE] No se encontraron vacantes');\n return [{\n json: {\n search_id: normalizeNode.search_id,\n user_id: normalizeNode.user_id,\n profile: normalizeNode.profile,\n vacancies: [],\n jobs_found: 0\n }\n }];\n}\n\nconst vacancies = allItems.map(item => ({\n id: item.json.id,\n title: item.json.title,\n company_name: item.json.company_name,\n city: item.json.city,\n work_modality: item.json.work_modality,\n contract_type: item.json.contract_type,\n required_education: item.json.required_education,\n required_experience_years: item.json.required_experience_years || 0,\n required_skills: typeof item.json.required_skills === 'string' \n ? JSON.parse(item.json.required_skills) \n : (item.json.required_skills || []),\n preferred_skills: typeof item.json.preferred_skills === 'string' \n ? JSON.parse(item.json.preferred_skills) \n : (item.json.preferred_skills || []),\n salary_min: item.json.salary_min,\n salary_max: item.json.salary_max,\n description: item.json.description,\n category: item.json.category\n}));\n\nconsole.log(`[AGGREGATE] ${vacancies.length} vacantes listas para scoring`);\n\nreturn [{\n json: {\n search_id: normalizeNode.search_id,\n user_id: normalizeNode.user_id,\n profile: normalizeNode.profile,\n vacancies: vacancies,\n jobs_found: vacancies.length\n }\n}];"
},
"id": "aggregate-jobs",
"name": "\ud83d\udce6 Agregar Vacantes",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE searches SET jobs_found = {{ $json.jobs_found }}, status = 'scoring' WHERE id = '{{ $json.search_id }}';",
"options": {}
},
"id": "update-jobs-found",
"name": "\ud83d\udcca Actualizar Conteo",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1340,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "http://scoring:8001/score",
"sendBody": true,
"contentType": "json",
"body": "={{ JSON.stringify({ profile: $('\ud83d\udce6 Agregar Vacantes').item.json.profile, vacancies: $('\ud83d\udce6 Agregar Vacantes').item.json.vacancies, top_n: 20 }) }}",
"options": {
"timeout": 30000
}
},
"id": "call-scoring",
"name": "\ud83e\udde0 Microservicio Scoring",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
300
]
},
{
"parameters": {
"jsCode": "// Preparar matches para insertar en PostgreSQL\nconst scoringResponse = $input.first().json;\nconst aggregateData = $('\ud83d\udce6 Agregar Vacantes').first().json;\n\nconst results = scoringResponse.results || [];\n\nconsole.log(`[SAVE] Guardando ${results.length} matches para search_id=${aggregateData.search_id}`);\n\nconst matchesForDB = results.map(r => ({\n search_id: aggregateData.search_id,\n user_id: aggregateData.user_id,\n job_id: r.job_id,\n total_score: r.total_score,\n skills_score: r.breakdown.skills_score,\n experience_score: r.breakdown.experience_score,\n location_score: r.breakdown.location_score,\n salary_score: r.breakdown.salary_score,\n education_score: r.breakdown.education_score,\n modality_score: r.breakdown.modality_score,\n semantic_score: r.breakdown.semantic_score,\n score_explanation: JSON.stringify(r.explanation),\n matched_skills: r.matched_skills,\n missing_skills: r.missing_skills,\n rank_position: r.rank_position,\n recommendation: r.recommendation\n}));\n\nreturn matchesForDB.map(m => ({ json: m }));"
},
"id": "prepare-matches",
"name": "\ud83d\udd04 Preparar Matches",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO matches (search_id, user_id, job_id, total_score, skills_score, experience_score, location_score, salary_score, education_score, modality_score, semantic_score, score_explanation, matched_skills, missing_skills, rank_position) VALUES ('{{ $json.search_id }}', '{{ $json.user_id }}', '{{ $json.job_id }}', {{ $json.total_score }}, {{ $json.skills_score }}, {{ $json.experience_score }}, {{ $json.location_score }}, {{ $json.salary_score }}, {{ $json.education_score }}, {{ $json.modality_score }}, {{ $json.semantic_score }}, '{{ $json.score_explanation }}', ARRAY[{{ $json.matched_skills.map(s => `'${s}'`).join(',') }}], ARRAY[{{ $json.missing_skills.map(s => `'${s}'`).join(',') }}], {{ $json.rank_position }}) ON CONFLICT (search_id, job_id) DO NOTHING;",
"options": {}
},
"id": "save-matches",
"name": "\ud83d\udcbe Guardar Matches",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2000,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE searches SET status = 'completed', jobs_scored = (SELECT COUNT(*) FROM matches WHERE search_id = '{{ $('\ud83d\udce6 Agregar Vacantes').item.json.search_id }}'), completed_at = NOW() WHERE id = '{{ $('\ud83d\udce6 Agregar Vacantes').item.json.search_id }}';",
"options": {}
},
"id": "complete-search",
"name": "\u2705 Completar B\u00fasqueda",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2220,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notifications (user_id, type, title, message, data) VALUES ('{{ $('\ud83d\udce6 Agregar Vacantes').item.json.user_id }}', 'matches_ready', '\u00a1Tus matches est\u00e1n listos!', 'Encontramos {{ $('\ud83e\udde0 Microservicio Scoring').item.json.results.length }} vacantes compatibles con tu perfil. Rev\u00edsalas ahora.', '{\"search_id\": \"{{ $('\ud83d\udce6 Agregar Vacantes').item.json.search_id }}\", \"count\": {{ $('\ud83e\udde0 Microservicio Scoring').item.json.results.length }}}') RETURNING id;",
"options": {}
},
"id": "create-notification",
"name": "\ud83d\udd14 Crear Notificaci\u00f3n",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2440,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Simular env\u00edo de email de notificaci\u00f3n\nconst userId = $('\ud83d\udce6 Agregar Vacantes').first().json.user_id;\nconst searchId = $('\ud83d\udce6 Agregar Vacantes').first().json.search_id;\nconst matchCount = $('\ud83e\udde0 Microservicio Scoring').first().json.results?.length || 0;\n\nconsole.log(`[EMAIL] Simulando email para user=${userId}`);\nconsole.log(`[EMAIL] Subject: \u00a1${matchCount} nuevas vacantes compatibles para ti!`);\nconsole.log(`[EMAIL] Body: Hemos encontrado ${matchCount} vacantes que coinciden con tu perfil.`);\nconsole.log(`[EMAIL] URL: http://localhost:3001/matches?search=${searchId}`);\n\n// En producci\u00f3n: usar SendGrid, AWS SES, etc.\n// const emailResult = await sendEmail({ to: userEmail, ... });\n\nreturn [{\n json: {\n email_sent: true,\n simulated: true,\n user_id: userId,\n match_count: matchCount,\n timestamp: new Date().toISOString()\n }\n}];"
},
"id": "send-email",
"name": "\ud83d\udce7 Notificaci\u00f3n Email (Simulado)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
300
]
},
{
"parameters": {
"method": "POST",
"url": "http://backend:3000/api/n8n/callback",
"sendBody": true,
"contentType": "json",
"body": "={{ JSON.stringify({ search_id: $('\ud83d\udce6 Agregar Vacantes').item.json.search_id, status: 'completed', matches_count: $('\ud83e\udde0 Microservicio Scoring').item.json.results?.length || 0 }) }}",
"options": {}
},
"id": "callback-backend",
"name": "\ud83d\udd01 Callback Backend",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: true, search_id: $('\ud83d\udce6 Agregar Vacantes').item.json.search_id, message: 'Workflow completado exitosamente', execution_id: $execution.id }) }}"
},
"id": "respond-webhook",
"name": "\u21a9\ufe0f Responder Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
3100,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO audit_logs (user_id, entity_type, entity_id, action, message, service, metadata) VALUES ('{{ $('\ud83d\udce6 Agregar Vacantes').item.json.user_id }}', 'search', '{{ $('\ud83d\udce6 Agregar Vacantes').item.json.search_id }}', 'WORKFLOW_COMPLETED', 'Workflow n8n ejecutado correctamente', 'n8n', '{\"execution_id\": \"{{ $execution.id }}\", \"matches\": {{ $('\ud83e\udde0 Microservicio Scoring').item.json.results?.length || 0 }}}');",
"options": {}
},
"id": "audit-log",
"name": "\ud83d\udccb Registro de Auditor\u00eda",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2440,
500
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
}
],
"connections": {
"\ud83d\ude80 Webhook Trigger": {
"main": [
[
{
"node": "\ud83d\udd27 Normalizar Datos",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Normalizar Datos": {
"main": [
[
{
"node": "\ud83d\udcdd Marcar como Procesando",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Marcar como Procesando": {
"main": [
[
{
"node": "\ud83d\uddc4\ufe0f Consultar Vacantes",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\uddc4\ufe0f Consultar Vacantes": {
"main": [
[
{
"node": "\ud83d\udce6 Agregar Vacantes",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce6 Agregar Vacantes": {
"main": [
[
{
"node": "\ud83d\udcca Actualizar Conteo",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Actualizar Conteo": {
"main": [
[
{
"node": "\ud83e\udde0 Microservicio Scoring",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udde0 Microservicio Scoring": {
"main": [
[
{
"node": "\ud83d\udd04 Preparar Matches",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd04 Preparar Matches": {
"main": [
[
{
"node": "\ud83d\udcbe Guardar Matches",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcbe Guardar Matches": {
"main": [
[
{
"node": "\u2705 Completar B\u00fasqueda",
"type": "main",
"index": 0
}
]
]
},
"\u2705 Completar B\u00fasqueda": {
"main": [
[
{
"node": "\ud83d\udd14 Crear Notificaci\u00f3n",
"type": "main",
"index": 0
},
{
"node": "\ud83d\udccb Registro de Auditor\u00eda",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd14 Crear Notificaci\u00f3n": {
"main": [
[
{
"node": "\ud83d\udce7 Notificaci\u00f3n Email (Simulado)",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce7 Notificaci\u00f3n Email (Simulado)": {
"main": [
[
{
"node": "\ud83d\udd01 Callback Backend",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd01 Callback Backend": {
"main": [
[
{
"node": "\u21a9\ufe0f Responder Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "",
"timezone": "America/Bogota"
},
"staticData": null,
"tags": [
{
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"id": "tag-empleabilidad",
"name": "empleabilidad-colombia"
}
],
"triggerCount": 0,
"updatedAt": "2024-01-01T00:00:00.000Z",
"versionId": "v1"
}
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.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Empleabilidad Colombia - Employment Search Workflow. Uses postgres, httpRequest. Webhook trigger; 15 nodes.
Source: https://github.com/JhonP16/LinkWork/blob/cb37a6cefe1f3b15866104711f2deffa3eae9775/n8n/workflow.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.
Scraping. Uses httpRequest, postgres, @apify/n8n-nodes-apify, respondToWebhook. Webhook trigger; 61 nodes.
Workflow B — AI Listing Engine. Uses httpRequest, postgres, errorTrigger. Webhook trigger; 47 nodes.
Fluxo de voluntárias ZendeskXANXBD. Uses functionItem, zendesk, httpRequest, postgres. Webhook trigger; 25 nodes.
Fluxo de voluntárias ZendeskXANXBD. Uses functionItem, zendesk, httpRequest, postgres. Webhook trigger; 25 nodes.
Fluxo de voluntárias ZendeskXANXBD. Uses functionItem, zendesk, httpRequest, postgres. Webhook trigger; 25 nodes.