How to Debug Workflows in Odoo 19: Complete Tutorial Guide
Debugging workflows in Odoo 19 is a skill every developer needs — whether you are investigating a state transition that silently fails, tracking down a rogue server action, or diagnosing why a frontend button produces no visible result. This complete tutorial covers how to read JSON-RPC error responses, enable server-side debug logging, attach Python's pdb debugger to model methods, intercept RPC calls in the browser, and use Odoo's exception hierarchy to surface errors correctly. This beginner guide to Odoo 19 debugging will save you hours of guesswork on every project.
What You'll Learn:
- Why Odoo 19 returns HTTP 200 even when workflow errors occur
- How to read the JSON-RPC error body and locate the data.name exception field
- How to enable granular server-side debug logging per module
- How to insert pdb breakpoints directly inside Odoo model methods
- How to intercept and log JSON-RPC calls from the Odoo web frontend
- When to raise UserError vs ValidationError vs AccessError
- How to query the ir_logging table directly for fast error triage
Why Odoo 19 Workflow Debugging Is Different
Odoo uses JSON-RPC over HTTP for all client-server communication. This means the HTTP transport layer always reports success — you will always see a 200 OK status code — while the actual application error lives inside the JSON response body under the error key. Developers who check the HTTP status code to detect failures will miss every application-level error Odoo raises.
Additionally, workflow state machines in Odoo 19 are not visualised anywhere in the UI by default. When a server action runs but produces no visible change, there is no error dialog — Odoo simply executed successfully against zero matching records. Understanding these two quirks is essential before picking up any debugging tool.
Server-Side Debug Logging
Enable granular logging via odoo.conf or the CLI startup command. Target a single module with --log-handler=odoo.addons.your_module:DEBUG to reduce noise without losing the information you need. All log output flows through Odoo's unified log-level filter.
Python pdb Debugger
Insert import pdb; pdb.set_trace() or just breakpoint() (Python 3.7+) directly into any Odoo model method. Odoo freezes at the breakpoint and waits for interactive console input. Step through state transitions one line at a time to isolate exactly where a workflow deviates from expected behaviour.
JSON-RPC Interception
Monitor Odoo's JSON-RPC traffic in the browser Network tab by filtering for XHR requests. For deeper inspection, temporarily wrap the rpc service to log both call arguments and responses to the console. Always remove this code before committing to version control.
Exception Handling
Odoo 19 provides a hierarchy of exception classes — UserError, ValidationError, AccessError, and MissingError — each mapped to a specific type of failure. Using the correct class ensures the frontend displays a meaningful dialog and that the error is logged with the right severity for later triage.
Step by Step Guide: Debugging Odoo 19 Workflows
This step by step guide walks you through the six core debugging techniques in Odoo 19, from understanding the error format through to querying the database for workflow state issues. Apply them in the order that fits your current problem — but read all six to build a complete mental toolkit.
Understand Odoo 19's JSON-RPC Error Response Structure
Open your browser DevTools and navigate to the Network tab. Filter for XHR or Fetch requests and trigger the workflow action that is failing. Click on the request and examine the Response body. You will see HTTP 200 regardless of whether Odoo's business logic succeeded or failed — this is by design because JSON-RPC separates transport from application status. Look for the top-level error key in the response JSON. Inside it, data.name contains the full Python exception class path (for example, odoo.exceptions.ValidationError) and data.message contains the human-readable error text. Note that the exception_type field was removed in Odoo 17 and is no longer present in Odoo 19 — always use data.name instead.
Enable Server-Side Debug Logging
Add log_level = debug and log_handler = :DEBUG to your odoo.conf file, then restart the server. Alternatively, pass --log-level=debug --log-handler=:DEBUG on the command line when starting Odoo. For targeted debugging without flooding the log with framework noise, use a module-specific handler: --log-handler=odoo.addons.your_module:DEBUG. This tells Odoo to write DEBUG-level messages only for your module while keeping all other modules at their default level. Watch the terminal or the log file — every method call, SQL query, and RPC handler will now print its entry and exit points so you can trace exactly which code path a workflow transition follows.
Use Python's pdb Debugger Inside Model Methods
Open the Python model file that contains the transition method you want to inspect. Add import pdb; pdb.set_trace() on the line just before the suspicious code, or simply write breakpoint() if you are on Python 3.7 or later. Restart Odoo in a terminal (not as a background service) so you have console access. When the workflow transition is triggered from the UI, Odoo freezes and drops you into the pdb interactive console. Use n to step to the next line, s to step into a function call, p variable_name to print a value, and c to continue execution. This lets you inspect self.state, self.amount_total, or any field value at exactly the moment the transition runs.
Intercept JSON-RPC Calls from the Frontend
For issues where the server logs look clean but the UI still misbehaves, the problem is often in the arguments being sent from the frontend. In your OWL component's setup() method, temporarily wrap the RPC service with a logging proxy: inject useService("rpc"), then replace it with an async function that calls console.log("[RPC Call]", args) before the original call and console.log("[RPC Result]", result) after it. Open the browser console and trigger the action to see exactly what payload the frontend is sending and what the server is returning. This technique works for debugging both button-triggered server calls and automated JavaScript-driven workflow transitions. Remove the wrapper completely before pushing any code to version control.
Handle Workflow Errors with Correct Odoo Exception Classes
When writing guard logic inside transition methods, always import from odoo.exceptions and raise the class that matches the failure type: UserError for blocked business processes, ValidationError for field constraint violations, AccessError for permission failures, and MissingError when a record has been deleted mid-transaction. Never raise a generic ValueError or Exception — Odoo's JSON-RPC error handler will still surface it, but the frontend dialog will show a less helpful message and the error will be harder to categorise in logs. Odoo-specific exceptions also trigger the correct transaction rollback behaviour, which protects data integrity when a workflow step fails partway through.
Query ir_logging Table for Workflow State Machine Issues
Odoo 19 writes all server-side log output to the ir_logging database table in addition to the terminal or log file. This is especially useful when you do not have direct server access but do have database access. Connect to the PostgreSQL database and run a SELECT against ir_logging filtered by level ERROR or WARNING, ordered by create_date DESC. You can also add a WHERE condition on the name column to filter by module. This approach is faster than tailing log files in environments where the log volume is high, and it lets you run aggregate queries to spot recurring errors across a time window.
Reading Odoo 19 JSON-RPC Error Responses
The JSON-RPC error body is the first place to look when a workflow action fails silently. The structure below shows a typical Odoo 19 error response. Always inspect error.data.name for the exception class and error.data.message for the user-facing text. The error.data.debug field contains the full Python traceback — paste this into your editor search to locate the exact line.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 200,
"message": "Odoo Server Error",
"data": {
"name": "odoo.exceptions.ValidationError",
"debug": "Traceback (most recent call last):
...",
"message": "The field 'email' is required.",
"arguments": ["The field 'email' is required."]
}
}
}
// ✓ Check response.error?.data?.name — exception class path (stable across versions)
// ✓ Check response.error?.data?.message — human-readable error text
// ✓ Check response.error?.data?.debug — full Python traceback
// ✗ Do NOT rely on HTTP status code — always 200 in JSON-RPC
// ✗ Do NOT use exception_type — removed in Odoo 17, absent in Odoo 19
Server-Side Debug Logging Configuration
Odoo's log system supports per-module granularity. The examples below show how to enable debug logging globally and how to target a single module to reduce log volume while debugging workflow logic. Restart Odoo after editing odoo.conf, or pass the flags directly on the startup command line for a quicker iteration loop.
# ── Method 1: odoo.conf (requires restart) ──────────────────
[options]
log_level = debug
log_handler = :DEBUG
# ── Method 2: CLI (no conf change needed) ────────────────────
python odoo-bin --log-level=debug --log-handler=:DEBUG -c odoo.conf
# ── Method 3: Target a single module (reduces noise) ─────────
python odoo-bin --log-level=info --log-handler=odoo.addons.your_module:DEBUG -c odoo.conf
# ── Method 4: Add custom _logger in Python model ─────────────
import logging
_logger = logging.getLogger(__name__)
def action_confirm(self):
_logger.debug("action_confirm called on %s (state=%s)", self.name, self.state)
return super().action_confirm()
Python pdb Debugger Inside Odoo Model Methods
Insert a breakpoint at any line in a model method. Odoo must be running in a foreground terminal — if it is managed by systemd or a process supervisor, stop the service and start it manually first so you have console access for pdb interactive input.
def action_confirm(self):
# ── Classic pdb (all Python versions) ───────────────────
import pdb; pdb.set_trace()
# ── Modern shorthand (Python 3.7+) ──────────────────────
breakpoint()
# Odoo freezes here — the browser shows a spinner.
# Switch to your terminal and use pdb commands:
# n → next line
# s → step into function
# c → continue execution
# q → quit (triggers a server error response)
# p self.state → print current state value
# p self.amount_total → print field value
return super().action_confirm()
# ── IMPORTANT: Remove ALL breakpoints before committing ──────
# Search your codebase before pushing:
# grep -rn "pdb.set_trace|breakpoint()" addons/your_module/
Never Leave pdb Breakpoints in Development or Production
If Odoo freezes unexpectedly — the browser spinner never stops and no error appears — it almost always means a pdb.set_trace() or breakpoint() call is blocking the server process, waiting for console input. Since no console is attached in a service environment, the process hangs indefinitely. Search your module with grep -rn "pdb|breakpoint" addons/your_module/ before any deploy and remove every instance. Use _logger.debug() for persistent diagnostic output instead.
Frontend JSON-RPC Interception with the OWL RPC Service
When server logs are clean but a button click still produces no result, the mismatch is often in the data the frontend sends. Temporarily wrapping the RPC service in your OWL component reveals the exact payload in the browser console without needing any server-side changes.
import { useService } from "@web/core/utils/hooks";
setup() {
// Inject the real RPC service
const originalRpc = useService("rpc");
// Wrap it to log every call and response
this.rpc = async (...args) => {
console.log("[RPC Call]", args);
const result = await originalRpc(...args);
console.log("[RPC Result]", result);
return result;
};
}
// Open the browser console (F12) and trigger your workflow button.
// You will see the exact route, method name, and arguments sent,
// plus the full response including any error.data.name value.
// ── REMOVE THIS WRAPPER BEFORE COMMITTING ────────────────────
Odoo 19 Exception Classes: When to Use Each
Choosing the right exception class makes debugging easier and keeps the frontend dialog informative. All four classes below are imported from odoo.exceptions and trigger the correct transaction rollback behaviour when raised inside a workflow method.
from odoo.exceptions import UserError, ValidationError, AccessError, MissingError
# ── UserError ────────────────────────────────────────────────
# Use for: blocked business logic (user made a wrong action)
# Frontend: shows a red warning dialog, no traceback
raise UserError("You can't confirm an order without a customer.")
# ── ValidationError ──────────────────────────────────────────
# Use for: field constraint violations, computed checks
# Frontend: shows a validation error dialog
raise ValidationError("The delivery date cannot be in the past.")
# ── AccessError ──────────────────────────────────────────────
# Use for: permission failures inside workflow methods
# Frontend: shows an access denied dialog
raise AccessError("You don't have rights to approve this record.")
# ── MissingError ─────────────────────────────────────────────
# Use for: record deleted or unlinked mid-transaction
# Frontend: shows a missing record error
raise MissingError("The target record no longer exists.")
# ── AVOID generic Python exceptions ──────────────────────────
# raise ValueError("...") — produces an ugly 'Internal Server Error'
# raise Exception("...") — hides the root cause from the user
Odoo Exception Classes: Quick Reference Table
| Exception Class | Use Case | Frontend Dialog | Rolls Back Transaction |
|---|---|---|---|
UserError |
Business logic blocked by user action | Red warning dialog, no traceback | Yes |
ValidationError |
Field constraint or computed check failure | Validation error dialog | Yes |
AccessError |
User lacks permission for this transition | Access denied dialog | Yes |
MissingError |
Target record deleted mid-transaction | Missing record dialog | Yes |
ValueError / Exception |
Generic Python — avoid in Odoo | Internal Server Error (unhelpful) | Yes |
Querying ir_logging for Workflow Error Triage
The ir_logging table stores all Odoo log records in the database. This is particularly useful when you cannot access the server terminal, when log file rotation has already removed recent entries, or when you want to run aggregate queries over a time window. Connect with any PostgreSQL client — psql, DBeaver, or pgAdmin — and run the query below.
-- Recent errors and warnings across all modules
SELECT name, level, message, create_date
FROM ir_logging
WHERE level IN ('ERROR', 'WARNING')
ORDER BY create_date DESC
LIMIT 50;
-- Filter to a single custom module
SELECT name, level, message, create_date
FROM ir_logging
WHERE level IN ('ERROR', 'WARNING')
AND name LIKE 'odoo.addons.your_module%'
ORDER BY create_date DESC
LIMIT 50;
-- Count errors per module in the last 24 hours
SELECT name, level, COUNT(*) as error_count
FROM ir_logging
WHERE level = 'ERROR'
AND create_date > NOW() - INTERVAL '24 hours'
GROUP BY name, level
ORDER BY error_count DESC;
Key Insight: Silent Server Actions Are Not Bugs
When a server action or automation rule runs without producing any visible change, there is no bug — Odoo executed successfully against zero matching records. The action's filter domain simply matched nothing. To confirm this, temporarily remove the domain filter and re-trigger the action. If it now runs, tighten the filter. If it still does nothing, check that the model name matches exactly and that the record you are testing is not already in the expected state. This is one of the most common sources of "nothing happened" confusion in Odoo 19 workflow debugging.
Frequently Asked Questions
Why does Odoo 19 return HTTP 200 even when a workflow error occurs?
Odoo communicates with the browser over JSON-RPC, which treats HTTP purely as a transport protocol. HTTP 200 means the request was received and a response was sent — it says nothing about whether the business logic succeeded. The actual application-level error is embedded inside the JSON body under the error key. Always inspect response.error?.data?.name and response.error?.data?.message in your client code, not the HTTP status.
What replaced the exception_type field removed in Odoo 17?
Use error.data.name instead. This field contains the full Python exception class path, such as odoo.exceptions.ValidationError, and has been stable since Odoo 17. It is available in all Odoo 19 versions. The exception_type field was removed during the HTTP refactoring in version 17 and is absent in all subsequent releases.
Odoo froze completely after I triggered a workflow — what happened?
The most common cause is a pdb.set_trace() or breakpoint() call left in a model method. The server process is paused waiting for interactive console input, but since no console is attached (or you forgot to switch to the terminal), it waits indefinitely. Search your module for pdb and breakpoint with grep -rn "pdb|breakpoint" addons/your_module/ and remove every instance, then restart Odoo.
What is the difference between UserError and ValidationError in Odoo 19?
UserError indicates that a business process cannot proceed because of a user action — for example, trying to confirm an order without a customer. ValidationError is for field-level constraint violations — for example, a delivery date set in the past. Both roll back the transaction and show a dialog. The distinction matters for code clarity and for internal constraint handling; Odoo may treat ValidationError differently in batch operations than UserError.
How can I add custom logging to an Odoo module without enabling full debug mode?
Import Python's standard logging module and create a logger at the module level: _logger = logging.getLogger(__name__). Then call _logger.info(), _logger.warning(), or _logger.debug() wherever you need output. Odoo routes all Python log output through its own log-level filter, so your messages only appear when the corresponding log level is enabled — they will not leak into production logs unless you explicitly set the handler level to DEBUG.
Need Expert Help Debugging Your Odoo 19 Workflows?
Our certified Odoo developers can diagnose and resolve complex workflow issues, state machine bugs, and integration failures — from root cause analysis through to tested, production-ready fixes.
About the author
Founder & Odoo Practice Lead, Braincuber Technologies
Founder of Braincuber. Has scoped and shipped 500+ Odoo implementations for US mid-market and global brands. Takes every founder call personally — no SDR layer between buyers and the people building the system.
