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": "parser_venta",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "wa/venta",
"responseMode": "responseNode"
},
"id": "wh-venta",
"name": "WhatsApp In",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "// Parsea el body de WhatsApp Cloud API y devuelve { texto, telefono }\nconst body = items[0].json.body || items[0].json;\nconst entry = body.entry?.[0]?.changes?.[0]?.value;\nconst msg = entry?.messages?.[0];\nif (!msg || msg.type !== 'text') return [];\nreturn [{ json: { texto: msg.text.body, telefono: msg.from } }];"
},
"id": "extract",
"name": "Extraer texto",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
300
]
},
{
"parameters": {
"jsCode": "// Parser del formato #VENTA. Devuelve estructura limpia.\nconst t = items[0].json.texto;\nconst tel = items[0].json.telefono;\nif (!/^#VENTA/i.test(t.trim())) return [];\n\nfunction grab(label) {\n const m = t.match(new RegExp('^\\\\s*' + label + '\\\\s*:\\\\s*(.+)$', 'im'));\n return m ? m[1].trim() : null;\n}\nfunction grabBlock(label) {\n const re = new RegExp(label + ':\\\\s*\\\\n([\\\\s\\\\S]*?)(?=\\\\n[A-Z_]+:|$)', 'i');\n const m = t.match(re); return m ? m[1] : '';\n}\n\nconst fecha = grab('FECHA') || new Date().toISOString().slice(0,10);\nconst total = parseFloat((grab('TOTAL')||'0').replace(/[^0-9.]/g,''));\nconst reserva = parseFloat((grab('RESERVA')||'0').replace(/[^0-9.]/g,''));\nconst envio = parseFloat((grab('ENVIO')||'0').replace(/[^0-9.]/g,''));\nconst notas = grab('NOTAS') || '';\n\nconst clienteBlock = grabBlock('CLIENTE');\nconst c = {};\nfor (const line of clienteBlock.split(/\\n/)) {\n const m = line.match(/^\\s*(\\w+)\\s*:\\s*(.+)$/);\n if (m) c[m[1].toLowerCase()] = m[2].trim();\n}\n\nconst prodBlock = grabBlock('PRODUCTOS');\nconst lineas = [];\nfor (const raw of prodBlock.split(/\\n/)) {\n const line = raw.replace(/^\\s*[-+]\\s*/, '').trim();\n if (!line) continue;\n const parts = line.split('|').map(s=>s.trim());\n const producto = parts.shift();\n const attrs = {};\n let cant = 1, precio = null;\n for (const p of parts) {\n const [k,v] = p.split('=').map(s=>s.trim());\n if (!k) continue;\n if (k.toLowerCase()==='cant') cant = parseInt(v,10) || 1;\n else if (k.toLowerCase()==='precio') precio = parseFloat(v);\n else attrs[k.toLowerCase()] = v;\n }\n lineas.push({ producto, attrs, cant, precio });\n}\n\nreturn [{ json: { vendedor_tel: tel, fecha, total, reserva, envio, notas, cliente: c, lineas } }];"
},
"id": "parse",
"name": "Parser #VENTA",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT proximo_numero('OV') AS numero;"
},
"id": "num",
"name": "Pr\u00f3ximo N\u00ba OV",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
840,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH up_cli AS (\n INSERT INTO clientes (nombre, dni, email, telefono, direccion, entre_calles, cp)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n),\nup_ven AS (\n INSERT INTO vendedores (nombre, whatsapp) VALUES ('auto', $8)\n ON CONFLICT (whatsapp) DO UPDATE SET activo = TRUE\n RETURNING id\n),\nins_ov AS (\n INSERT INTO ordenes_venta (numero, fecha, cliente_id, vendedor_id, estado, total, reserva, adeudado, costo_envio, notas)\n VALUES ($9, $10::date, (SELECT id FROM up_cli), (SELECT id FROM up_ven),\n 'borrador', $11, $12, $11 - $12, $13, $14)\n RETURNING id, numero\n)\nSELECT * FROM ins_ov;",
"options": {
"queryReplacement": "={{ $json.cliente.nombre }},{{ $json.cliente.dni }},{{ $json.cliente.email }},{{ $json.cliente.tel }},{{ $json.cliente.direccion }},{{ $json.cliente.entre_calles }},{{ $json.cliente.cp }},{{ $json.vendedor_tel }},{{ $node['Pr\u00f3ximo N\u00ba OV'].json.numero }},{{ $json.fecha }},{{ $json.total }},{{ $json.reserva }},{{ $json.envio }},{{ $json.notas }}"
}
},
"id": "ins-ov",
"name": "Insert OV + cliente",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1060,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Para cada l\u00ednea pedida resuelve la variante o falla con error legible.\nconst ov = items[0].json;\nconst src = $node['Parser #VENTA'].json;\nconst out = [];\nfor (const l of src.lineas) {\n out.push({ json: { ov_id: ov.id, ov_numero: ov.numero, linea: l } });\n}\nreturn out;"
},
"id": "expand",
"name": "Expandir l\u00edneas",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1280,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Busca variante exacta por nombre de producto + set de valores de atributos (clave=valor).\nWITH attrs AS (\n SELECT key AS atributo, value AS valor\n FROM jsonb_each_text($1::jsonb)\n),\np AS (SELECT id FROM productos WHERE lower(nombre) = lower($2))\nSELECT v.id, v.sku, COALESCE(v.precio, pr.precio_base) AS precio\nFROM variantes v JOIN productos pr ON pr.id = v.producto_id\nWHERE v.producto_id = (SELECT id FROM p)\n AND NOT EXISTS (\n SELECT 1 FROM attrs a\n WHERE NOT EXISTS (\n SELECT 1 FROM variante_valores vv\n JOIN valores_atributo va ON va.id = vv.valor_atributo_id\n JOIN atributos at ON at.id = va.atributo_id\n WHERE vv.variante_id = v.id\n AND lower(at.nombre) = lower(a.atributo)\n AND lower(va.valor) = lower(a.valor)\n )\n )\nLIMIT 1;",
"options": {
"queryReplacement": "={{ JSON.stringify($json.linea.attrs) }},{{ $json.linea.producto }}"
}
},
"id": "find-var",
"name": "Resolver variante",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1500,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO lineas_ov (ov_id, variante_id, cantidad, precio_unit)\nVALUES ($1, $2, $3, $4) RETURNING id;",
"options": {
"queryReplacement": "={{ $node['Expandir l\u00edneas'].json.ov_id }},{{ $json.id }},{{ $node['Expandir l\u00edneas'].json.linea.cant }},{{ $node['Expandir l\u00edneas'].json.linea.precio || $json.precio }}"
}
},
"id": "ins-lin",
"name": "Insert l\u00ednea OV",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1720,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"url": "=http://n8n:5678/webhook/internal/stock_y_oc",
"method": "POST",
"jsonBody": "={ \"ov_id\": {{ $node['Insert OV + cliente'].json.id }} }",
"sendBody": true
},
"id": "trigger-stock",
"name": "Disparar stock_y_oc",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1940,
300
]
},
{
"parameters": {
"url": "=https://graph.facebook.com/v20.0/{{$env.WHATSAPP_PHONE_ID}}/messages",
"method": "POST",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{$env.WHATSAPP_TOKEN}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $node['Parser #VENTA'].json.vendedor_tel }}\",\n \"type\": \"text\",\n \"text\": { \"body\": \"OV creada: {{ $node['Insert OV + cliente'].json.numero }}. Te aviso cuando est\u00e9 el link de pago y la OC al proveedor.\" }\n}"
},
"id": "wa-out",
"name": "Confirmar al vendedor",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={ \"ok\": true, \"ov\": $node['Insert OV + cliente'].json.numero }"
},
"id": "resp",
"name": "Responder OK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
2380,
300
]
}
],
"connections": {
"WhatsApp In": {
"main": [
[
{
"node": "Extraer texto",
"type": "main",
"index": 0
}
]
]
},
"Extraer texto": {
"main": [
[
{
"node": "Parser #VENTA",
"type": "main",
"index": 0
}
]
]
},
"Parser #VENTA": {
"main": [
[
{
"node": "Pr\u00f3ximo N\u00ba OV",
"type": "main",
"index": 0
}
]
]
},
"Pr\u00f3ximo N\u00ba OV": {
"main": [
[
{
"node": "Insert OV + cliente",
"type": "main",
"index": 0
}
]
]
},
"Insert OV + cliente": {
"main": [
[
{
"node": "Expandir l\u00edneas",
"type": "main",
"index": 0
}
]
]
},
"Expandir l\u00edneas": {
"main": [
[
{
"node": "Resolver variante",
"type": "main",
"index": 0
}
]
]
},
"Resolver variante": {
"main": [
[
{
"node": "Insert l\u00ednea OV",
"type": "main",
"index": 0
}
]
]
},
"Insert l\u00ednea OV": {
"main": [
[
{
"node": "Disparar stock_y_oc",
"type": "main",
"index": 0
}
]
]
},
"Disparar stock_y_oc": {
"main": [
[
{
"node": "Confirmar al vendedor",
"type": "main",
"index": 0
}
]
]
},
"Confirmar al vendedor": {
"main": [
[
{
"node": "Responder OK",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "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
parser_venta. Uses postgres, httpRequest. Webhook trigger; 11 nodes.
Source: https://github.com/bytemok/P/blob/420be2ab06f74f6499aa4a2b44761aec596e2005/workflows/parser_venta.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.