Key Takeaways: Not every Odoo alert belongs inside Odoo. For scheduled checks and notifications, n8n is often a cleaner option than customization. The hardest part of this workflow is not the API call; it is defining the right business filter and shaping the output correctly. Use an API key, not a login password, and verify the numeric user ID separately. A wrong ID can cause confusing failures. Never send one email per matching record when the business expects one summary. Consolidate first, then notify. In n8n, the Code node is the simplest place to merge multiple Odoo records into one operational email.
A confirmed sales order with no invoice is rarely a technical problem. It’s an operational one. The order moves forward, the team stays busy, and invoicing slips until someone in finance spots the gap, usually later than anyone would like.
Automating this check sounds straightforward. In practice, it’s the kind of workflow that gets implemented wrong more often than not. The system finds the right records, but sends one email per order and turns a useful alert into something people start ignoring within a week.
This post walks through a concrete use case: using n8n with Odoo 18 Enterprise to scan confirmed sales orders that still have no invoice and deliver one consolidated email every 15 minutes. The API calls are not complicated. The design is what matters: fetch the right records, group them before sending, and format the alert so someone will actually act on it.
Why n8n Instead of an Odoo Customization?
Before getting into the workflow, it’s worth asking the obvious question: why reach for n8n at all?
Because this type of requirement often doesn’t justify a customization inside Odoo. If the business need is to scan records on a schedule, apply some filter logic, and send a notification, an external workflow layer is usually enough.
n8n gives you three practical advantages here:
- No ERP customization needed. The requirement is operational visibility, not a change to Odoo’s core logic. An external scheduler handles this without touching the codebase.
- Full control over alert behavior. The real challenge isn’t pulling records from Odoo. It’s deciding how the alert should behave. One email per order? One summary? Different notifications by team or company? n8n lets you design that behavior without changing anything inside Odoo.
- Easy to extend later. Once the pattern exists, you can reuse it: Slack, Teams, escalations, spreadsheet logs, dashboards. Same structure, different outputs.
The architectural point: Odoo stays the system of record. n8n handles the scheduling, the aggregation, and the delivery. That’s a clean division of responsibility.
What the Workflow Should Actually Do
The business goal is simple: send one email listing all confirmed sales orders that still have no invoice.
Not one email per order. One summary per scheduled run.
Here’s the expected behavior:
- Run every 15 minutes
- Query Odoo for
sale.orderrecords insalestate with no linked invoice - Consolidate all matches into one HTML summary
- Send that summary through Odoo’s own mail server
That last detail matters. Routing the alert through Odoo’s internal mail infrastructure keeps everything inside one system and avoids setting up a separate SMTP connection unless the project specifically calls for it.
System Setup
- Source system: Odoo 18 Enterprise
- Automation layer: n8n (self-hosted or cloud)
- Email channel: Odoo internal mail server via
mail.mail
Step 1: Authenticate n8n with Odoo
Use an API key, not your Odoo login password.
In Odoo, go to Settings > Users & Companies > Users, open the relevant user, then navigate to the Account Security tab to generate a new API key. Store it somewhere secure.
You’ll also need that user’s numeric ID. Open the user record and check the URL:
https://your-odoo.com/odoo/settings/users/6
The number at the end (here, 6) is the user ID. You’ll pass this alongside the API key in every JSON-RPC call.
Gotcha: A wrong user ID won’t always throw an obvious error. Some workflows appear to run but never complete. If authentication feels flaky, verify this ID first.
Step 2: Schedule the Workflow
Add a Schedule node in n8n and set it to run every 15 minutes.
That interval surfaces invoicing gaps early without flooding inboxes. You can tighten or loosen it later depending on how time-sensitive the invoicing process is for your client.
Step 3: Query Odoo for Confirmed but Uninvoiced Orders
Use an HTTP Request node to call Odoo’s JSON-RPC endpoint:
POST https://your-odoo-url.com/jsonrpc
Call search_read on the sale.order modelA mathematical function trained on data that maps inputs to outputs. In ML, a model is the artifact produced after training — it encapsulates learned patterns and is used to make predictions or…. At minimum, your domain filter should target:
[["state", "=", "sale"], ["invoice_ids", "=", false]]
This returns sales orders that are confirmed (state = sale) but have no related invoice (invoice_ids is empty).
In real projects, you’ll almost always need to refine this further. Common additions:
- Filter by company if the Odoo instance runs multiple entities
- Exclude orders confirmed in the last 30–60 minutes (to avoid false alerts on very recent confirmations)
- Filter by sales team or business unit if different teams have different invoicing SLAs
This is where functional understanding matters. A filter that’s technically correct can still produce operationally wrong results if the logic is too broad.
Fields worth requesting: name, partner_id, date_order, amount_total, user_id
Step 4: Consolidate Before You Send Anything
This is the most important design decision in the entire workflow. Get it wrong and the automation becomes the thing people complain about.
When n8n receives multiple sale.order records from Odoo, it treats them as separate items. Connect those items directly to an email step and you’ll send one email per order. That’s rarely what anyone wants.
The wrong pattern: Query Odoo → get 10 orders → send 10 emails.
The right pattern: Query Odoo → get 10 orders → run a Code node → produce 1 HTML summary → send 1 email.
Your Code node should combine all matching orders into a single block:
const orders = $input.all();
const rows = orders.map(o => {
const d = o.json;
return `<tr>
<td>${d.name}</td>
<td>${d.partner_id[1]}</td>
<td>${d.date_order}</td>
<td>${d.amount_total}</td>
<td>${d.user_id[1]}</td>
</tr>`;
}).join('');
const html = `
<h3>Uninvoiced Confirmed Sales Orders — ${new Date().toLocaleString()}</h3>
<p><strong>${orders.length} order(s)</strong> confirmed but not yet invoiced.</p>
<table border="1" cellpadding="6" cellspacing="0">
<thead>
<tr><th>Order</th><th>Customer</th><th>Date</th><th>Amount</th><th>Salesperson</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
return [{ json: { html_summary: html, count: orders.length } }];
The exact HTML structure doesn’t matter much. What matters is that this node returns one item, not ten. That single design choice is what prevents the workflow from becoming noise.
Step 5: Send the Summary Through Odoo
Use another HTTP Request node to call jsonrpc again, this time creating a record on the mail.mail modelA mathematical function trained on data that maps inputs to outputs. In ML, a model is the artifact produced after training — it encapsulates learned patterns and is used to make predictions or…:
{
"jsonrpc": "2.0",
"method": "call",
"params": {
"service": "object",
"method": "execute_kw",
"args": [
"your-db-name",
6,
"your-api-key",
"mail.mail",
"create",
[{
"subject": "Uninvoiced Confirmed Sales Orders - Action Required",
"body_html": "{{ $json.html_summary }}",
"email_to": "[email protected]",
"auto_delete": true
}]
]
}
}
Then call send on the created record ID to trigger delivery. Odoo handles the rest through its own mail queue.
What a Good Alert Email Looks Like
The email should answer four things immediately:
- What’s the problem?
- How many orders are affected?
- Which orders specifically?
- What should the recipient do?
Keep it operational. No decorative formatting. A clean table with order reference, customer, date, and amount does the job. Add the salesperson column if the finance team needs to know who to chase.
The subject line should be specific: “Uninvoiced Confirmed Sales Orders - Action Required” works better than a generic “Odoo Alert.”
Common Issues and How to Fix Them
- Workflow spins but never finishes. Usually a wrong user ID in the JSON-RPC payload. The API key looks fine, so this one is easy to miss. Go back to the Odoo user record URL and confirm the numeric ID.
- Still getting one email per order. The Code node isn’t consolidating correctly, or you’ve connected the raw query output directly to the mail step. Check that the Code node returns exactly one item:
return [{ json: { ... } }], not a mapped array of items. - “Access Denied” error. Wrong API key, expired key, or the user doesn’t have Sales read access. Test with a user that has standard Sales permissions and work backward from there.
- Orders showing up that shouldn’t. Your domain filter is too broad. Common culprits: missing company filter on multi-company instances, or orders from test customers included. Refine the domain and test with
search_readdirectly in a REST client before wiring it into n8n.
Where This Design Works and Where It Doesn’t
This workflow is a good fit when:
- The number of matching orders per run stays manageable (say, under 50)
- One summary email per run is enough for the business
- The invoicing team just needs visibility, not a full acknowledgment loop
It starts to show its limits when clients ask for more: different recipients per salesperson, reminder escalation after X hours, a log of which alerts have been acted on, or a dashboard showing resolution rates. At that point, you’re probably looking at additional fields or states inside Odoo itself, not just an external scheduler.
This workflow is a solid first step. It’s not a process governance system.
A Note for Functional Consultants
You don’t need to write JavaScript to define this workflow well. The part that actually requires expertise is the business logic, and that’s yours.
Before handing anything to a developer or an AI tool, be able to answer:
- What exactly counts as “uninvoiced” for this client? (Is a partially invoiced order a problem? What about draft invoices?)
- Which sales orders should be included, and which should be excluded?
- Who receives the alert? Finance only, or also the salesperson?
- How often should it run? Is 15 minutes right, or does the business need hourly?
- What level of detail is actually useful in the message?
Once those answers are sharp, the implementation (the JSON-RPC payloads, the Code node, the HTML template) becomes a straightforward task for any developer or a well-prompted AI tool.
The consultant’s job is to make the requirement precise enough that implementation can’t go wrong.
The Pattern Behind This Use Case
This example solves one alert. But the structure generalizes to most Odoo operational checks:
- Purchase orders pending approval too long
- Overdue customer invoices
- CRM leads without follow-up activity
- Support tickets approaching SLA breach
- Orders stuck in a specific stage
- Missing delivery documentation before invoicing
In every case, the structure is the same:
- Query Odoo
- Filter the right records with correct business logic
- Consolidate before sending
- Deliver one meaningful notification
That pattern, not the specific use case, is what’s worth taking away from this.
Want help designing the filter logic or adapting this pattern for another Odoo alert? Reach out to the Trobz team. We can share the workflow JSON and walk through the business logic for your specific case.