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": "job.application_logger",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "ef639907-3887-45a9-a5db-5654ffab87a9",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-176,
0
],
"id": "11113675-c9dc-479c-b963-7a27cca79550",
"name": "Extension Listener"
},
{
"parameters": {
"method": "POST",
"url": "http://enhancer-api:8000/enhance",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"url\": \"{{ $json.body.url }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
16,
0
],
"id": "0473321f-ae03-4c16-bdf2-1965d35d89db",
"name": "HTTP Request"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1d262426-2501-4e1e-94b5-02151323af5b",
"name": "company",
"value": "={{ $json.company.replace('.jobs', '').replace('.com', '').replace('.io', '').replace('.co.*', '') }}",
"type": "string"
},
{
"id": "b264a54e-af74-4090-81d0-02ef5a2b0ea6",
"name": "position",
"value": "={{ $json.position }}",
"type": "string"
},
{
"id": "6d09bf27-bf5d-4018-9eaf-df8d538f3825",
"name": "source",
"value": "={{ $json.source }}",
"type": "string"
},
{
"id": "d9adf7e3-d19b-4eb1-8919-6603ce55248e",
"name": "applyUrl",
"value": "={{ $json.applyUrl }}",
"type": "string"
},
{
"id": "875d3c02-c05d-4c23-880f-df12834599dd",
"name": "jobId",
"value": "={{ $json.jobId }}",
"type": "string"
},
{
"id": "a3caead8-6b45-48e4-953f-d26ac16a6f49",
"name": "url",
"value": "={{ $json.url }}",
"type": "string"
},
{
"id": "a9faf965-9c10-4f6c-9393-31a5f4e88ed3",
"name": "metadata",
"value": "={{\n{\n \"url_domain\": \"$json.metadata.url_domain\",\n \"title\": \"$json.metadata.title\"\n}\n}}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
224,
0
],
"id": "432e7d42-c446-47a7-bb28-2bdad751dc68",
"name": "Set data"
},
{
"parameters": {
"jsCode": "// --- Helper Function: Normalizes Company Name ---\nfunction normalizeCompanyName(name) {\n if (!name) return null; // Handle null or empty names\n let normalized = name.toLowerCase().trim();\n normalized = normalized.replace(/\\s+(inc|llc|ltd|corp|corporation|group|company|co)\\.?$/g, '');\n normalized = normalized.replace(/^(the|a|an)\\s+/g, '');\n normalized = normalized.replace(/\\s+(the|a|an)$/g, '');\n normalized = normalized.replace(/[^a-z0-9\\s]/g, ''); // Keep only alphanumeric and spaces\n normalized = normalized.replace(/\\s+/g, ' '); // Collapse multiple spaces\n return normalized.trim();\n}\n\n// --- Helper Function: Extracts Company from URL (Simplified) ---\nfunction extractCompanyFromUrl(url) {\n if (!url) return null;\n try {\n const urlObj = new URL(url);\n let hostname = urlObj.hostname;\n hostname = hostname.replace(/^www\\./, ''); // Remove www.\n\n // Try to get the main domain part\n const parts = hostname.split('.');\n if (parts.length > 1) {\n // If it's something like example.com or jobs.example.com, take 'example'\n const companyPart = (parts[parts.length - 2] || parts[0]).trim();\n if (companyPart && companyPart !== 'co' && companyPart.length > 1) { // Basic sanity\n return companyPart.charAt(0).toUpperCase() + companyPart.slice(1);\n }\n }\n } catch (error) {\n // console.error('Error parsing URL for company:', error);\n }\n return null;\n}\n\n// --- Helper Function: Extracts Company from Metadata (Simplified) ---\nfunction extractCompanyFromMetadata(metadata) {\n if (!metadata) return null;\n\n // Try to find \"Company Name - Job Title\" or \"Job Title | Company Name\"\n let match = metadata.match(/^(.*?)\\s*[-\u2013\u2014|]\\s*(.*)$/);\n if (match) {\n // Check if the first part looks like a company or the second part does\n let potentialCompany = match[1].trim();\n let potentialCompany2 = match[2].trim();\n\n // Prioritize if it doesn't look like a job title\n const jobTitleKeywords = ['engineer', 'developer', 'manager', 'analyst', 'senior', 'junior', 'staff', 'principal', 'lead'];\n const looksLikeJobTitle = (text) => jobTitleKeywords.some(keyword => text.toLowerCase().includes(keyword));\n\n if (!looksLikeJobTitle(potentialCompany) && potentialCompany.length > 2) {\n return potentialCompany;\n }\n if (!looksLikeJobTitle(potentialCompany2) && potentialCompany2.length > 2) {\n return potentialCompany2;\n }\n }\n\n // Simple fallback: take the first word if it looks capitalized and is long enough\n match = metadata.match(/^([A-Z][a-zA-Z\\s]+)/);\n if (match && match[1].trim().length > 2) {\n return match[1].trim();\n }\n\n return null;\n}\n\n\n// --- Main Processing Logic ---\n\n// Safely access webhook data\nconst webhookData = $input.item.json.body || $input.item.json;\n\n// Validate required fields (can throw error if essential data is missing)\nif (!webhookData.url && !webhookData.applyUrl) {\n throw new Error('Missing required field: url or applyUrl');\n}\n\nconst currentTimestamp = new Date().toISOString();\n\n// Attempt to extract company name in a prioritized order\nlet companyGuess = webhookData.company || null; // 1. From extension's direct scrape\n\nif (!companyGuess) { // 2. From URL\n companyGuess = extractCompanyFromUrl(webhookData.applyUrl || webhookData.url);\n}\n\nif (!companyGuess) { // 3. From metadata (page title)\n companyGuess = extractCompanyFromMetadata(webhookData.metadata);\n}\n\n// Normalize the best guess\nconst finalCompanyName = normalizeCompanyName(companyGuess);\n\n\n// Create output data for the job_applications table\nconst outputData = {\n company_name: finalCompanyName, // Use the normalized company name or null\n role_title: webhookData.position || null,\n role_id: webhookData.jobId || null, // Your role_id for job_applications table\n url: webhookData.applyUrl || webhookData.url, // Ensure URL is never null\n metadata: webhookData.metadata || null, // Keep the raw metadata\n source: webhookData.source || 'extension', // Default source\n\n // FSM Initial State\n current_state: 'APPLIED',\n applied_date: currentTimestamp,\n last_state_change: currentTimestamp,\n\n // Default nulls for other fields\n upcoming_date: null,\n date_context: null\n};\n\n// Return array of items (n8n expects an array)\nreturn [outputData];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
0
],
"id": "33b2df14-2dbe-4bf7-a080-7486bc041f7d",
"name": "transition company name to lowercase"
},
{
"parameters": {
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"table": {
"__rl": true,
"value": "job_applications",
"mode": "list",
"cachedResultName": "job_applications"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"role_title": "={{ $json.role_title }}",
"current_state": "={{ $json.current_state }}",
"applied_date": "={{ $json.applied_date }}",
"last_state_change": "={{ $json.last_state_change }}",
"source": "={{ $json.source }}",
"role_id": "={{ $json.role_id }}",
"company_name": "={{ $json.company_name }}",
"url": "={{ $json.url }}",
"metadata": "={{ $json.metadata }}"
},
"matchingColumns": [],
"schema": [
{
"id": "application_id",
"displayName": "application_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "role_title",
"displayName": "role_title",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "current_state",
"displayName": "current_state",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "applied_date",
"displayName": "applied_date",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true
},
{
"id": "last_state_change",
"displayName": "last_state_change",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true
},
{
"id": "source",
"displayName": "source",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "created_at",
"displayName": "created_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "role_id",
"displayName": "role_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "upcoming_date",
"displayName": "upcoming_date",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "date_context",
"displayName": "date_context",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "company_name",
"displayName": "company_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "url",
"displayName": "url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "metadata",
"displayName": "metadata",
"required": false,
"defaultMatch": false,
"display": true,
"type": "object",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
656,
0
],
"id": "35d34168-3dd4-4d8b-b169-b1234b86ea60",
"name": "Insert into job application table",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Extension Listener": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Set data",
"type": "main",
"index": 0
}
]
]
},
"Set data": {
"main": [
[
{
"node": "transition company name to lowercase",
"type": "main",
"index": 0
}
]
]
},
"transition company name to lowercase": {
"main": [
[
{
"node": "Insert into job application table",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"availableInMCP": false,
"timeSavedMode": "fixed",
"callerPolicy": "workflowsFromSameOwner",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveExecutionProgress": true,
"saveManualExecutions": false
},
"versionId": "ff1a24e5-797d-4f3f-8b05-fddd362bb70c",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "hIuXtkcbmdht9e4B",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
job.application_logger. Uses httpRequest, postgres. Webhook trigger; 5 nodes.
Source: https://github.com/daemon-Ad/Job-Aggregation-and-Email-Intelligence-System/blob/main/workflows/job.application_logger.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.
CMM. Uses httpRequest, postgres, redis. Webhook trigger; 90 nodes.
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.
LogSentinel Workflow. Uses postgres, emailSend, httpRequest. Webhook trigger; 44 nodes.
Post-Prayer. Uses postgres, httpRequest. Webhook trigger; 44 nodes.