This is not a marketing blog about what automation can do for you. You already know that, or you can refer our other post.. This is the guide you open on your second monitor while you are actually setting things up.
We are going to walk through a full end-to-end integration where leads come in from any source, OpenAI reads and qualifies them, and everything lands in Odoo CRM as a structured opportunity with the right salesperson assigned, the right tags applied, and a personalised follow-up email already sent. Every step has the actual configuration, the actual code, and the actual things that go wrong.
Let us get into it.
What We Are Building
By the end of this guide you will have a working system that does the following:
A lead fills a form on your website or sends an email inquiry. n8n picks it up instantly. OpenAI reads what they wrote, scores the lead, extracts their name, company, intent, and budget signals, and writes a personalised first reply. n8n then creates a lead in Odoo CRM with all of that enriched data, assigns it to the right salesperson based on the lead type, and sends the personalised email. The salesperson gets a Slack notification with a one-line summary. The whole thing takes under 30 seconds from form submission to CRM entry.
Here is the full stack we are using:
- n8n (self-hosted or cloud)
- OpenAI GPT-4o via API
- Odoo CRM (any version from 14 onwards)
- A form tool (we will use Tally but any webhook-capable form works)
- Gmail or SMTP for outbound email
- Slack for team notifications
Part 1: Setting Up n8n
Option A — n8n Cloud
Go to n8n.io and sign up for an account. The Starter plan at $24 per month gives you enough for everything in this guide. Once you are in, you see the workflow canvas. That is where everything gets built.
Skip ahead to Part 2 if you are using Cloud.
Option B — Self-Hosted on a VPS
This is worth doing properly if you are going to run this for a real business. Get a VPS from Hetzner, DigitalOcean, or any provider. A $6 per month instance with 2GB RAM is enough to start.
SSH into your server and run the following.
# Update and install Docker sudo apt update && sudo apt upgrade -y sudo apt install docker.io docker-compose -y # Create a directory for n8n mkdir ~/n8n && cd ~/n8n # Create the docker-compose file nano docker-compose.yml
Paste this into the docker-compose.yml file:
version: '3.8'
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: always
ports:
- "5678:5678"
environment:
- N8N_HOST=yourdomain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- WEBHOOK_URL=https://yourdomain.com/
- GENERIC_TIMEZONE=Asia/Kolkata
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=choose_a_strong_password
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=choose_a_db_password
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
postgres:
image: postgres:15
restart: always
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=choose_a_db_password
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
postgres_data:
Replace yourdomain.com with your actual domain or server IP. Change the passwords to something strong. Then run:
docker-compose up -d
Now point a domain to your server and set up an Nginx reverse proxy so n8n runs on HTTPS. Here is the Nginx config:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300;
proxy_connect_timeout 300;
}
}
Get an SSL certificate with Certbot:
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d yourdomain.com sudo nginx -t && sudo systemctl reload nginx
Go to https://yourdomain.com and log into n8n with the credentials you set.
Part 2: Connecting OpenAI
Get your OpenAI API key from platform.openai.com. Under your account, go to API Keys and create a new key. Copy it immediately because you will not see it again.
In n8n:
- Click the menu in the top right corner
- Go to Settings then Credentials
- Click Add Credential
- Search for OpenAI and select it
- Paste your API key
- Name it something like "OpenAI Production"
- Save
That is it. The credential is now available in any workflow you build.
One important thing about costs. GPT-4o is priced per token. For lead qualification, each run costs roughly $0.002 to $0.008 depending on how much text the lead submitted. At 500 leads per month that is around $2 to $4 in OpenAI costs. Keep an eye on your usage dashboard at platform.openai.com during the first month.
If you want to keep costs lower during testing, switch to gpt-4o-mini in the model field. It is noticeably cheaper and handles lead qualification well enough for most use cases.
Part 3: Setting Up Odoo CRM
This guide assumes you have Odoo running. If you are on Odoo.com cloud, the setup is the same. If you are self-hosted, make sure your instance is accessible from the internet so n8n can reach it.
Enable the CRM module
In Odoo, go to Apps and make sure CRM is installed. If it is not, install it.
Create an API user in Odoo
Do not use your admin account for API calls. Create a dedicated user.
Go to Settings then Users then New User.
Name: n8n Integration Email: n8n@yourcompany.com Access Rights: Set CRM access to User
Save the user, then go to their profile and set a password.
Get your Odoo API credentials
Odoo uses XML-RPC for its API. You need three things:
- Your Odoo URL (example: https://yourcompany.odoo.com)
- The database name (find this in Settings then General Settings, it shows the database name at the top)
- The username and password of the API user you just created
Test the Odoo connection manually
Before building anything in n8n, confirm the API is working. Run this Python script from your local machine or server:
import xmlrpc.client
url = 'https://yourcompany.odoo.com'
db = 'your_database_name'
username = 'n8n@yourcompany.com'
password = 'your_api_user_password'
# Authenticate
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
print(f'Connected. User ID: {uid}')
# Test by fetching the first CRM lead
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
leads = models.execute_kw(
db, uid, password,
'crm.lead', 'search_read',
[[]],
{'fields': ['name', 'partner_name', 'email_from'], 'limit': 3}
)
for lead in leads:
print(lead)
If you see "Connected. User ID: X" and a list of leads, you are good. If you get an authentication error, double check the database name and credentials.
Add Odoo as a credential in n8n
n8n does not have a native Odoo credential type but it does not need one. Odoo's API calls go through HTTP. We use the HTTP Request node with credentials embedded in the request. We will cover the exact configuration in the workflow steps below.
Part 4: Setting Up the Lead Capture Form
We are using Tally for the form because it has a native webhook and is free. The same approach works with Typeform, Jotform, a custom HTML form, or any tool that can POST data to a URL.
Create a new form in Tally with these fields:
- Full Name (text, required)
- Company Name (text, required)
- Email Address (email, required)
- Phone Number (phone, optional)
- What are you looking for? (long text, required)
- How soon do you need this? (dropdown: Immediately, Within a month, Just exploring)
- Monthly budget range (dropdown: Under $500, $500 to $2000, $2000 to $10000, $10000 plus)
Go to Tally's Integrations tab and select Webhooks. We will paste the n8n webhook URL here in the next step.
Part 5: Building the Workflow in n8n
This is the main part. We are building this as a single workflow with multiple nodes. Open n8n and create a new workflow. Name it "Lead Capture and Odoo Sync."
Node 1: Webhook Trigger
Add a Webhook node as the first node.
- HTTP Method: POST
- Path: lead-capture
- Authentication: None (we will use a secret token instead)
- Response Mode: Respond Immediately
- Response Code: 200
Once you add this node, n8n shows you the webhook URL. It will look like this:
https://yourdomain.com/webhook/lead-capture
Copy that URL and paste it into Tally's webhook settings. Submit a test form entry from Tally and check the Webhook node's output in n8n to see the raw data structure. You need to know the exact field names that Tally sends.
The Tally payload looks something like this when you inspect it:
{
"eventId": "abc123",
"eventType": "FORM_RESPONSE",
"createdAt": "2025-02-27T10:30:00.000Z",
"data": {
"responseId": "xyz789",
"fields": [
{
"key": "question_full_name",
"label": "Full Name",
"value": "Rahul Sharma"
},
{
"key": "question_company",
"label": "Company Name",
"value": "TechStart Pvt Ltd"
},
{
"key": "question_email",
"label": "Email Address",
"value": "rahul@techstart.in"
},
{
"key": "question_message",
"label": "What are you looking for?",
"value": "We need help setting up a CRM and automating our sales process. We have around 30 people in sales and currently using spreadsheets."
},
{
"key": "question_timeline",
"label": "How soon do you need this?",
"value": "Within a month"
},
{
"key": "question_budget",
"label": "Monthly budget range",
"value": "$2000 to $10000"
}
]
}
}
Node 2: Extract Fields (Set Node)
Add a Set node after the Webhook. This extracts the fields from Tally's nested structure into flat variables that are easier to work with in later nodes.
Click Add Field for each of these:
Name: full_name Value: {{ $json.data.fields.find(f => f.label === 'Full Name')?.value || '' }}
Name: company_name Value: {{ $json.data.fields.find(f => f.label === 'Company Name')?.value || '' }}
Name: email Value: {{ $json.data.fields.find(f => f.label === 'Email Address')?.value || '' }}
Name: phone Value: {{ $json.data.fields.find(f => f.label === 'Phone Number')?.value || '' }}
Name: message Value: {{ $json.data.fields.find(f => f.label === 'What are you looking for?')?.value || '' }}
Name: timeline Value: {{ $json.data.fields.find(f => f.label === 'How soon do you need this?')?.value || '' }}
Name: budget Value: {{ $json.data.fields.find(f => f.label === 'Monthly budget range')?.value || '' }}
Name: submitted_at Value: {{ $json.createdAt }}
After this node runs, you have clean flat variables to pass into the AI.
Node 3: OpenAI Lead Analysis
Add an OpenAI node. Select "Message a Model" as the operation.
Model: gpt-4o (or gpt-4o-mini for lower cost) Credential: OpenAI Production (the one you created earlier)
Under Messages, set Role to System and paste this as the content:
You are a lead qualification assistant for a B2B technology company.
Your job is to analyse an inbound lead and return a structured JSON response.
Analyse the lead based on the information provided and return ONLY a valid JSON object with no markdown, no code blocks, and no additional text.
The JSON must follow this exact structure:
{
"lead_score": <integer from 1 to 10>,
"lead_quality": "<Hot | Warm | Cold>",
"primary_intent": "<one short sentence describing what the lead wants>",
"pain_point": "<the main business problem they mentioned>",
"budget_signal": "<High | Medium | Low | Unknown>",
"urgency": "<High | Medium | Low>",
"recommended_salesperson": "<Enterprise | SMB | Startup>",
"tags": ["<tag1>", "<tag2>", "<tag3>"],
"personalised_reply": "<a 3 to 4 sentence reply that sounds natural and human, addresses their specific situation, and invites them to book a call. Do not use their first name more than once. Do not start with Hi or Hello.>",
"internal_summary": "<one sentence summary for the sales team Slack notification>"
}
Scoring guide:
- Score 8 to 10: Clear need, defined budget, urgent timeline, decision maker signals
- Score 5 to 7: Clear need but budget or timeline is vague
- Score 1 to 4: Very early stage, wrong fit, or insufficient information
Add a second message with Role set to User and paste this:
Name: {{ $json.full_name }}
Company: {{ $json.company_name }}
Email: {{ $json.email }}
Timeline: {{ $json.timeline }}
Budget: {{ $json.budget }}
Message: {{ $json.message }}
Under Options, set Response Format to JSON. Enable the option if available in your n8n version, or just rely on the system prompt instructing the model to return only JSON.
After this node runs, you can see the AI's full response in the output. The content field contains the JSON string. We parse it in the next step.
Node 4: Parse AI Response (Code Node)
Add a Code node. Set Language to JavaScript and paste the following:
const items = $input.all();
const result = [];
for (const item of items) {
// Get the raw content from OpenAI's response
const rawContent = item.json.message?.content || item.json.choices?.[0]?.message?.content || '';
let aiData = {};
try {
// Clean the string in case there are any stray backticks or markdown
const cleaned = rawContent
.replace(/```json/g, '')
.replace(/```/g, '')
.trim();
aiData = JSON.parse(cleaned);
} catch (e) {
// If parsing fails, set safe defaults so the workflow continues
aiData = {
lead_score: 5,
lead_quality: 'Warm',
primary_intent: 'Inbound inquiry',
pain_point: 'Not parsed',
budget_signal: 'Unknown',
urgency: 'Medium',
recommended_salesperson: 'SMB',
tags: ['inbound', 'needs-review'],
personalised_reply: `Thank you for reaching out. We have received your inquiry and someone from our team will be in touch shortly to discuss how we can help.`,
internal_summary: 'Lead received. AI parsing failed. Needs manual review.'
};
}
// Merge original lead data with AI analysis
result.push({
json: {
// Original fields
full_name: item.json.full_name,
company_name: item.json.company_name,
email: item.json.email,
phone: item.json.phone,
message: item.json.message,
timeline: item.json.timeline,
budget: item.json.budget,
submitted_at: item.json.submitted_at,
// AI fields
lead_score: aiData.lead_score,
lead_quality: aiData.lead_quality,
primary_intent: aiData.primary_intent,
pain_point: aiData.pain_point,
budget_signal: aiData.budget_signal,
urgency: aiData.urgency,
recommended_salesperson: aiData.recommended_salesperson,
tags: aiData.tags || [],
personalised_reply: aiData.personalised_reply,
internal_summary: aiData.internal_summary
}
});
}
return result;
After this node, every field from both the form and the AI analysis is available as a flat variable. This is what the rest of the workflow works with.
Node 5: Route by Lead Quality (Switch Node)
Add a Switch node. This sends Hot leads, Warm leads, and Cold leads down different paths.
Mode: Rules Input field: {{ $json.lead_quality }}
Add three rules:
Rule 1: Value equals "Hot" → Output 0 Rule 2: Value equals "Warm" → Output 1 Rule 3: Value equals "Cold" → Output 2
Each output connects to a different version of the next steps, or you can merge them after and use conditions within single nodes. For simplicity, we will use one path from here and pass the quality as a variable.
Node 6: Create Lead in Odoo CRM
Add an HTTP Request node. This calls the Odoo XML-RPC API to create a CRM lead.
The Odoo XML-RPC API for creating records expects a specific format. Here is how to configure the node:
Method: POST URL: https://yourcompany.odoo.com/xmlrpc/2/object Authentication: None (we send credentials in the body) Body Content Type: XML
Body:
<?xml version="1.0"?>
<methodCall>
<methodName>execute_kw</methodName>
<params>
<param><value><string>your_database_name</string></value></param>
<param><value><int>YOUR_API_USER_UID</int></value></param>
<param><value><string>your_api_user_password</string></value></param>
<param><value><string>crm.lead</string></value></param>
<param><value><string>create</string></value></param>
<param>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>name</name>
<value><string>{{ $json.company_name }} — {{ $json.primary_intent }}</string></value>
</member>
<member>
<name>partner_name</name>
<value><string>{{ $json.company_name }}</string></value>
</member>
<member>
<name>contact_name</name>
<value><string>{{ $json.full_name }}</string></value>
</member>
<member>
<name>email_from</name>
<value><string>{{ $json.email }}</string></value>
</member>
<member>
<name>phone</name>
<value><string>{{ $json.phone }}</string></value>
</member>
<member>
<name>description</name>
<value><string>Lead Message: {{ $json.message }}
AI Analysis:
Score: {{ $json.lead_score }}/10
Quality: {{ $json.lead_quality }}
Intent: {{ $json.primary_intent }}
Pain Point: {{ $json.pain_point }}
Budget Signal: {{ $json.budget_signal }}
Urgency: {{ $json.urgency }}
Timeline: {{ $json.timeline }}
Budget Range: {{ $json.budget }}</string></value>
</member>
<member>
<name>priority</name>
<value><string>{{ $json.lead_score >= 8 ? '2' : $json.lead_score >= 5 ? '1' : '0' }}</string></value>
</member>
<member>
<name>tag_ids</name>
<value>
<array><data></data></array>
</value>
</member>
</struct>
</value>
</data>
</array>
</value>
</param>
<param><value><struct></struct></value></param>
</params>
</methodCall>
Replace your_database_name, YOUR_API_USER_UID, and your_api_user_password with your real values. The UID is the number you got when you ran the Python test script earlier.
Important note about priority in Odoo: Odoo CRM uses 0, 1, and 2 for priority stars. 0 is normal, 1 is one star, 2 is two stars, and 3 is three stars (highest). We map the AI score directly to these.
The response from Odoo will be XML containing the new lead ID. We parse this in the next step.
Node 7: Extract Odoo Lead ID (Code Node)
Add another Code node to extract the lead ID from Odoo's XML response:
const items = $input.all();
const result = [];
for (const item of items) {
const xmlResponse = item.json.data || '';
// Extract the integer value from the XML response
// Odoo returns something like: <value><int>123</int></value>
const match = xmlResponse.match(/<int>(\d+)<\/int>/);
const leadId = match ? parseInt(match[1]) : null;
result.push({
json: {
...item.json,
odoo_lead_id: leadId,
odoo_lead_url: leadId
? `https://yourcompany.odoo.com/odoo/crm/${leadId}`
: null
}
});
}
return result;
Now you have the Odoo lead ID and a direct link to the lead, both available for the Slack notification.
Node 8: Send Personalised Email
Add a Gmail node (or any SMTP node if you prefer).
Operation: Send Email To: {{ $json.email }} Subject: Re: Your enquiry about {{ $json.primary_intent }} Body Type: HTML Body:
<p>{{ $json.personalised_reply }}</p>
<p>If you would like to jump on a quick call, you can pick a time that works for you here: <a href="https://calendly.com/yourlink">Book a call</a></p>
<p>Looking forward to speaking with you.</p>
<p>—<br>
Your Name<br>
Your Company<br>
+91 99999 99999</p>
From Name: Your Name at Your Company Reply To: your@company.com
Node 9: Slack Notification
Add a Slack node.
Operation: Post a Message Channel: #new-leads (or whatever your sales channel is) Message:
*New Lead — {{ $json.lead_quality }} ({{ $json.lead_score }}/10)*
*Company:* {{ $json.company_name }}
*Contact:* {{ $json.full_name }} — {{ $json.email }}
*Summary:* {{ $json.internal_summary }}
*Budget:* {{ $json.budget }} · *Timeline:* {{ $json.timeline }}
*Urgency:* {{ $json.urgency }} · *Budget Signal:* {{ $json.budget_signal }}
<{{ $json.odoo_lead_url }}|Open in Odoo CRM>
For the Slack credential, go to api.slack.com and create an app in your workspace. Give it chat:write permissions and add it to the channel. Get the Bot Token (starts with xoxb) and add it as a credential in n8n.
Node 10: Error Handler (Webhook node at the end)
Add an Error Trigger node as a separate workflow or connect an error branch. In the main workflow, click the three dots on any node that could fail and select "Add Error Output." Connect these to a Slack node that sends a message to a private channel:
*Workflow Error*
Lead from: {{ $json.email || 'unknown' }}
Error: {{ $json.error.message }}
Node: {{ $json.error.node.name }}
Time: {{ $now.format('DD MMM YYYY HH:mm') }}
This way you know immediately when something breaks instead of finding out three days later that leads have been silently failing.
Part 6: Workflow for Email Inquiries
If leads also come in through a general inbox like sales@yourcompany.com, here is how to handle those automatically.
Create a second workflow. Use the Email Trigger (IMAP) node.
Host: your mail server or imap.gmail.com for Gmail Port: 993 SSL: Yes User: sales@yourcompany.com Password: your email password or app password
The node polls the inbox every minute. When a new email arrives, the workflow fires.
Add a Code node after the trigger to extract what you need:
const items = $input.all();
const result = [];
for (const item of items) {
const email = item.json;
// Clean the email body (remove excessive whitespace and HTML tags)
const cleanBody = (email.text || email.html || '')
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 2000); // Limit to 2000 chars to control token usage
// Try to extract name from email
const fromName = email.from?.value?.[0]?.name || '';
const fromEmail = email.from?.value?.[0]?.address || '';
result.push({
json: {
full_name: fromName,
company_name: fromEmail.split('@')[1]?.split('.')[0] || 'Unknown',
email: fromEmail,
phone: '',
message: cleanBody,
subject: email.subject || '',
timeline: 'Unknown',
budget: 'Unknown',
source: 'email_inbound'
}
});
}
return result;
From here, connect to the same OpenAI node and the rest of the workflow. The same lead scoring and Odoo creation logic applies. Email inquiries and form submissions end up in the same CRM pipeline with the same enrichment.
Part 7: Assign Leads to Salespersons Automatically
This is the extra step that makes the whole system genuinely useful for a team.
In Odoo, go to CRM settings and note the user ID of each salesperson. You can find these by going to Settings then Users and clicking on each user. The ID is in the URL bar when you open their profile.
Create a mapping in n8n using a Code node after the AI analysis step:
const items = $input.all();
const result = [];
// Map salesperson categories to Odoo user IDs
// Replace these IDs with your actual Odoo user IDs
const salespersonMap = {
'Enterprise': 8, // Odoo user ID for your enterprise sales rep
'SMB': 12, // Odoo user ID for your SMB sales rep
'Startup': 15 // Odoo user ID for your startup-focused rep
};
for (const item of items) {
const category = item.json.recommended_salesperson || 'SMB';
const userId = salespersonMap[category] || salespersonMap['SMB'];
result.push({
json: {
...item.json,
odoo_user_id: userId
}
});
}
return result;
Then in the Odoo create lead XML body, add this field inside the struct:
<member>
<name>user_id</name>
<value><int>{{ $json.odoo_user_id }}</int></value>
</member>
Now leads are automatically assigned to the right person the moment they come in.
Part 8: Testing the Full Workflow
Before going live, run through this checklist:
Submit a test lead through the Tally form. Check that the Webhook node received the data. Check that the Set node extracted all fields correctly. Check the OpenAI node output and confirm the JSON parsed correctly. Check that the Odoo HTTP Request returned a numeric lead ID. Open Odoo CRM and find the new lead. Confirm the description, priority, and assigned user are correct. Check your inbox for the personalised email. Check Slack for the notification. Check that the Odoo lead URL in the Slack message opens the correct lead.
Do this three or four times with different types of input. Test a high-intent message, a vague message, and a very short message. Make sure the AI handles all of them without returning malformed JSON. The Code node fallback we built in Node 4 catches any parse failures, but you want to confirm it actually works.
Part 9: Going Live and Monitoring
Activate the workflow using the toggle in the top right of the n8n canvas.
In n8n, go to Executions in the left sidebar. Every workflow run appears here. You can see which runs succeeded, which failed, and exactly what data was in each node at the time. This is your first monitoring tool.
Set up n8n's built-in error notifications under Settings then Events. You can send yourself an email whenever any workflow fails.
Check the workflow executions every day for the first two weeks. After that, once you are confident it is stable, check weekly.
Common Things That Go Wrong and How to Fix Them
The Odoo API returns an authentication error
Double check the UID. Run the Python script again and confirm the number. Make sure the API user has not been deactivated in Odoo.
OpenAI returns a response but JSON.parse fails
Add a console.log of the raw content in your Code node temporarily to see what the model is actually returning. Sometimes the model adds a trailing comma or a comment if the prompt is ambiguous. Tighten the system prompt instruction to say "return only valid JSON, no trailing commas, no comments."
The Tally webhook is not triggering
Make sure the webhook URL in Tally includes the correct path. Go to the Webhook node in n8n, copy the Test URL during development and the Production URL when active. These are different. Using the Test URL in production is a very common mistake.
Leads are being created in Odoo but the priority is not setting correctly
Odoo's priority field expects a string, not an integer. The values "0", "1", "2", "3" are strings in XML-RPC. The expression in the XML body handles this but double check the XML is sending it as a string type, not an int type.
The email is landing in spam
This is a deliverability problem, not a workflow problem. Set up SPF, DKIM, and DMARC for your sending domain. If you are using Gmail to send, use Google Workspace with proper authentication. Avoid sending from a free Gmail address for business emails.
What This Costs to Run Monthly
Once everything is set up and running:
| Component | Monthly Cost |
| n8n Cloud Starter | $24 |
| n8n Self-Hosted (VPS) | $6 to $10 |
| OpenAI API (500 leads/month, GPT-4o) | $3 to $8 |
| OpenAI API (500 leads/month, GPT-4o-mini) | $0.50 to $1.50 |
| Odoo Community (self-hosted) | $0 |
| Odoo.com Online (Starter) | $24.90 per user |
| Tally (form) | Free |
| Gmail / Google Workspace | $6 per user |
| Slack (free tier) | $0 |
| Total minimal setup | $34 to $60/month |
| Total with Odoo cloud | $60 to $100/month |
How Bithost Helps You Get This Running
If reading through the steps above was useful but you would rather have someone set it up correctly the first time, this is what Bithost does.
We have built this exact stack for businesses at different stages. Some already had Odoo running and needed the automation layer. Some were starting from scratch and needed Odoo set up before the automation could even begin. Some had a working n8n instance but the workflows were fragile and undocumented. Every situation is a bit different.
Here is how we approach it.
First, we understand your actual process. Not the process as it should be but the process as it actually runs today. Who handles incoming leads? Where do they come from? What does a good lead look like for your business versus a bad one? What happens after the first reply? These answers shape what gets built.
Then we set everything up properly from the start. That means n8n on a server that is configured for reliability, not just functionality. It means Odoo configured so the CRM pipeline actually reflects your sales stages. It means the AI prompts are tuned to your industry and your lead profile, not a generic template. It means every workflow has error handling and every failure sends an alert.
We test with your real data before we hand anything over. We run the workflow against actual form submissions or past lead emails. We show you the output in Odoo and in Slack before we call it done. You see it working before you trust it.
You get documentation that your team can actually use. A written guide for each workflow that explains what it does, what to check if something breaks, and how to adjust the AI prompts when your business needs change. Not a technical document. Something a non-developer can follow.
The 30 days after go-live are included. This is when real issues surface. A new form field breaks the extraction. A new Odoo version changes an API response. An edge case the AI handles poorly shows up in production. We handle all of this within the included support window.
If you want to get this running without spending three weekends figuring out XML-RPC responses and Docker networking, reach out.
Bithost builds automation systems for marketing, sales, and operations teams using n8n, OpenAI, Odoo or other ERP, and whatever else your stack needs. Based in India, working with businesses globally.