GuideMarch 13, 2026

Recruitment in Odoo 19:
Job Postings, Applicant Tracking & Hiring Pipeline

INTRODUCTION

Your Recruitment Process Lives in 6 Spreadsheets and 3 Inboxes. That Ends Today.

Here is the pattern we see at every mid-market company before they adopt a proper ATS: job descriptions live in Google Docs, resumes arrive in a shared inbox that three people monitor (and two ignore), interview feedback sits in Slack threads that nobody can search, and the "pipeline" is an Excel sheet updated once a week by whoever remembers. When a hiring manager asks "how many candidates are in stage 2 for the senior dev role?", the answer takes 20 minutes and is wrong.

Odoo 19's Recruitment module replaces this chaos with a single, integrated hiring pipeline that connects directly to your HR employee records, payroll configuration, and company website. This guide walks through the complete setup: configuring job positions, publishing online postings, tracking applicants through a Kanban pipeline, scheduling interviews with calendar integration, sending offer letters, and converting hired candidates into HR employees — all without leaving Odoo.

01

How to Configure Job Positions and Departments in Odoo 19 Recruitment

Job positions are the foundation of the recruitment module. Each position defines a role, links to a department, sets the expected number of hires, and determines the recruitment pipeline stages. Before posting anything externally, you need to get this structure right.

Step 1: Enable the Recruitment Module

Navigate to Apps and install the hr_recruitment module. This also pulls in hr (Employees) and calendar (for interview scheduling) as dependencies.

Python — __manifest__.py (if extending recruitment)
{
    'name': 'Custom Recruitment Extensions',
    'version': '19.0.1.0.0',
    'depends': ['hr_recruitment', 'website_hr_recruitment'],
    'data': [
        'security/ir.model.access.csv',
        'views/hr_applicant_views.xml',
    ],
    'license': 'LGPL-3',
}

Step 2: Create Department Structure

Go to Recruitment → Configuration → Departments. Each department should have a manager assigned — this person will be the default responsible for recruitment in that department. The hierarchy matters because interview scheduling and approval workflows reference it.

XML — Example department data
<odoo>
  <data noupdate="1">
    <record id="dept_engineering" model="hr.department">
      <field name="name">Engineering</field>
      <field name="manager_id" ref="base.user_admin"/>
    </record>

    <record id="dept_product" model="hr.department">
      <field name="name">Product</field>
      <field name="parent_id" ref="dept_engineering"/>
    </record>
  </data>
</odoo>

Step 3: Create Job Positions

Navigate to Recruitment → Configuration → Job Positions. Each position represents a role you are actively (or will be) hiring for. Key fields:

FieldPurposeBest Practice
Job PositionName displayed on careers page and in internal viewsUse clear titles: "Senior Backend Developer" not "Dev III"
DepartmentLinks to HR department hierarchyRequired for proper pipeline filtering
Expected New EmployeesTarget headcount for this positionSet to actual hiring target — pipeline reports use this as the denominator
Recruitment ResponsibleHR person who owns this pipelineDefaults to department manager; override for dedicated recruiters
Interview FormCustom survey sent to interviewers for structured feedbackCreate one per role type (technical, cultural, management)
Job Positions vs. Job Titles

A Job Position is a recruitment-facing entity with a pipeline. A Job Title (on the employee record) is the internal label shown in the org chart. They can differ: the position "Senior Backend Developer (Python/Odoo)" recruits the candidate, but the employee record shows "Senior Developer". Keep position titles descriptive for search engines; keep job titles clean for internal use.

02

Publishing Job Postings on Your Odoo 19 Website and External Job Boards

The website_hr_recruitment module turns your Odoo website into a careers page. Each published job position gets a public URL, an application form, and automatic indexing in the recruitment pipeline.

Step 1: Install Website Recruitment

Install the website_hr_recruitment module from Apps. This adds a /jobs route to your website with a filterable list of open positions.

Step 2: Configure the Job Description

Open any job position and click "Go to Website". The website editor opens with a template for the job posting. You can edit the description, add requirements, benefits, and salary range using the website builder's drag-and-drop blocks.

XML — Custom application form field (extend the default)
<template id="custom_application_fields"
          inherit_id="website_hr_recruitment.apply">
  <xpath expr="//div[hasclass('o_website_hr_recruitment_form')]//div[@class='row'][last()]"
         position="after">
    <div class="row mb-3">
      <div class="col-12">
        <label for="x_portfolio_url" class="form-label">
          Portfolio URL
        </label>
        <input type="url"
               name="x_portfolio_url"
               class="form-control"
               placeholder="https://your-portfolio.com"/>
      </div>
    </div>
  </xpath>
</template>

Step 3: Publish and Manage Visibility

Each position has a "Published" toggle. When published, it appears on /jobs and accepts applications. When unpublished, existing applicants remain in the pipeline but new applications are blocked. Use the "Website Published" boolean in filters to track which positions are live.

Python — Automated job board posting via server action
# Example: Server action to push new postings to external API
# Triggered when a job position is published

for record in records:
    if record.website_published:
        payload = {
            'title': record.name,
            'department': record.department_id.name,
            'description': record.description or '',
            'location': record.address_id.city or 'Remote',
            'apply_url': f'{{record.website_url}}/apply',
        }
        # POST to your job board aggregator API
        import requests
        requests.post(
            'https://api.yourjobboard.com/v1/postings',
            json=payload,
            headers={'Authorization': f'Bearer {{env["ir.config_parameter"].sudo().get_param("jobboard.api_key")}}'},
            timeout=30,
        )
SEO for Job Postings

Odoo 19 generates JobPosting structured data (JSON-LD) automatically for published positions when the website_hr_recruitment module is installed. Verify it with Google's Rich Results Test. If the structured data is missing salary or location info, override the _get_structured_data method on hr.job to include baseSalary and jobLocation fields — this dramatically improves visibility in Google for Jobs.

03

Applicant Tracking in Odoo 19: From Application to Interview in Under 5 Minutes

Every application — whether submitted through the website form, created manually, or imported via email alias — creates an hr.applicant record in the Kanban pipeline. Here's how to set up the tracking system so nothing falls through the cracks.

Step 1: Configure Email Aliases

Each job position can have a dedicated email alias (e.g., jobs-engineering@yourcompany.com). Any email sent to this alias automatically creates an applicant record with the email body as the description and attachments (resume, cover letter) linked to the record. Configure this in Recruitment → Configuration → Job Positions → [Position] → Email Alias.

Python — Automated applicant tagging via server action
# Server action: Auto-tag applicants based on source
# Model: hr.applicant | Trigger: On Creation

for applicant in records:
    tags = []

    # Tag by source channel
    if applicant.source_id:
        source_name = applicant.source_id.name.lower()
        if 'linkedin' in source_name:
            tags.append(env.ref('hr_recruitment.tag_linkedin'))
        elif 'website' in source_name:
            tags.append(env.ref('hr_recruitment.tag_website'))

    # Tag by resume keywords (basic screening)
    if applicant.description:
        desc_lower = applicant.description.lower()
        if 'odoo' in desc_lower or 'erp' in desc_lower:
            tags.append(env.ref('my_module.tag_erp_experience'))
        if 'python' in desc_lower:
            tags.append(env.ref('my_module.tag_python'))

    if tags:
        applicant.categ_ids = [(4, t.id) for t in tags]

Step 2: Configure Recruitment Stages

Stages define the columns in your Kanban pipeline. Odoo ships with defaults, but you should customize them to match your actual hiring process. Navigate to Recruitment → Configuration → Stages.

StagePurposeAutomated ActionSLA Target
NewApplication received, pending initial reviewAuto-assign to recruiter via round-robin<= 24 hours to first review
Initial ScreeningResume reviewed, phone screen scheduledSend acknowledgement email template<= 48 hours to phone screen
Technical InterviewTechnical assessment or coding challengeNotify hiring manager, create calendar event<= 5 business days
Culture FitTeam interview, values alignmentSend interview survey to panel<= 3 business days after technical
OfferOffer letter generated and sentGenerate offer from salary template<= 2 business days after final interview
HiredOffer accepted, employee record createdCreate hr.employee, trigger onboardingSame day as acceptance
XML — Custom recruitment stages data
<odoo>
  <data noupdate="1">
    <record id="stage_new" model="hr.recruitment.stage">
      <field name="name">New</field>
      <field name="sequence">1</field>
      <field name="fold">False</field>
      <field name="template_id" ref="email_template_applicant_ack"/>
    </record>

    <record id="stage_screening" model="hr.recruitment.stage">
      <field name="name">Initial Screening</field>
      <field name="sequence">2</field>
      <field name="requirements">Review resume. Schedule 15-min phone call.</field>
    </record>

    <record id="stage_technical" model="hr.recruitment.stage">
      <field name="name">Technical Interview</field>
      <field name="sequence">3</field>
      <field name="requirements">Coding challenge completed and reviewed.</field>
    </record>

    <record id="stage_culture" model="hr.recruitment.stage">
      <field name="name">Culture Fit</field>
      <field name="sequence">4</field>
    </record>

    <record id="stage_offer" model="hr.recruitment.stage">
      <field name="name">Offer</field>
      <field name="sequence">5</field>
    </record>

    <record id="stage_hired" model="hr.recruitment.stage">
      <field name="name">Hired</field>
      <field name="sequence">6</field>
      <field name="fold">True</field>
      <field name="hired_stage">True</field>
    </record>
  </data>
</odoo>
04

Interview Scheduling with Odoo 19 Calendar Integration

Odoo 19 integrates the recruitment module with the Calendar app so interviews are first-class calendar events — not email threads asking "does 3 PM Tuesday work for everyone?" The interviewer, the candidate (via email), and the recruiter all see the event on their calendars.

Step 1: Schedule Directly from the Applicant Record

Open any applicant and click "Schedule Interview". This opens a calendar event form pre-filled with the applicant name, the job position, and the responsible recruiter. Add interviewers as attendees, set the duration, and optionally include a video call link.

Python — Extend interview scheduling with auto-generated meet links
from odoo import models, fields, api


class HrApplicant(models.Model):
    _inherit = 'hr.applicant'

    def action_schedule_interview(self):
        """Override to auto-attach video meeting link."""
        action = super().action_schedule_interview()

        # Pre-fill context with default values
        ctx = action.get('context', {})
        ctx.update({
            'default_videocall_location': self._generate_meet_url(),
            'default_duration': 1.0,  # 1 hour default
            'default_partner_ids': [
                (4, self.partner_id.id),          # Candidate
                (4, self.user_id.partner_id.id),  # Recruiter
            ],
        })
        action['context'] = ctx
        return action

    def _generate_meet_url(self):
        """Generate a unique meeting URL."""
        import uuid
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'recruitment.meet_base_url',
            'https://meet.yourcompany.com',
        )
        return f'{{base_url}}/{{uuid.uuid4().hex[:12]}}'

Step 2: Interview Feedback via Surveys

Odoo 19 supports interview evaluation forms built with the Survey module. Assign a survey template to each job position. After an interview, each panelist fills out the structured form — scores are aggregated on the applicant record for objective comparison.

Python — Compute average interview score on applicant
class HrApplicant(models.Model):
    _inherit = 'hr.applicant'

    x_avg_interview_score = fields.Float(
        string='Avg Interview Score',
        compute='_compute_avg_score',
        store=True,
    )

    @api.depends('survey_user_input_ids', 'survey_user_input_ids.scoring_percentage')
    def _compute_avg_score(self):
        for applicant in self:
            inputs = applicant.survey_user_input_ids.filtered(
                lambda i: i.state == 'done'
            )
            if inputs:
                applicant.x_avg_interview_score = sum(
                    inputs.mapped('scoring_percentage')
                ) / len(inputs)
            else:
                applicant.x_avg_interview_score = 0.0
Structured Interviews Reduce Bias

Without structured forms, interview feedback devolves into "I liked them" or "not a culture fit" — subjective, legally risky, and impossible to compare across candidates. Odoo's survey integration forces interviewers to score specific competencies on a scale. This produces quantifiable, defensible hiring decisions and dramatically reduces unconscious bias. We configure every client's recruitment module with at least one scored survey per interview stage.

05

Mastering the Odoo 19 Recruitment Kanban: Filters, Colors, and Automation Rules

The Kanban view is where recruiters spend 80% of their time. Out of the box it works, but with a few customizations it becomes a real-time command center for your hiring pipeline.

Pipeline Metrics That Matter

Use Odoo's built-in reporting (Recruitment → Reporting) to track these KPIs:

MetricWhat It MeasuresHealthy BenchmarkOdoo Field
Time to HireDays from application to offer accepted<= 30 days for standard rolesdate_closed - create_date
Stage Conversion Rate% of applicants moving to next stageScreen → Interview: 25-40%Pipeline analysis report
Source EffectivenessWhich channels produce hiresTrack cost-per-hire by sourcesource_id grouped by stage
Pipeline VelocityAverage days per stage<= 5 days per stagedate_last_stage_update delta
Offer Acceptance Rate% of offers that convert to hires>= 85%Offer stage → Hired stage ratio

Automated Actions for Pipeline Hygiene

Stale applicants kill pipeline accuracy. Here are automated rules we configure for every client:

Python — Automated action: Flag stale applicants
# Scheduled action: Run daily
# Model: hr.applicant

from datetime import timedelta

stale_threshold = fields.Date.today() - timedelta(days=14)

stale_applicants = env['hr.applicant'].search([
    ('stage_id.hired_stage', '=', False),
    ('stage_id.fold', '=', False),
    ('active', '=', True),
    ('date_last_stage_update', '<=', str(stale_threshold)),
])

for applicant in stale_applicants:
    applicant.priority = '0'  # Reset priority to flag review
    applicant.message_post(
        body='This applicant has been in the current stage for over 14 days. '
             'Please review and take action.',
        message_type='notification',
        subtype_xmlid='mail.mt_note',
        partner_ids=[applicant.user_id.partner_id.id],
    )
Python — Automated action: Send rejection email on refuse
# Server action triggered when applicant is refused
# Model: hr.applicant | Trigger: On Stage Change (to refused)

for applicant in records:
    if applicant.active is False:  # Odoo archives on refuse
        template = env.ref(
            'hr_recruitment.email_template_applicant_refuse'
        )
        if template and applicant.partner_id.email:
            template.send_mail(applicant.id, force_send=True)
Kanban Color Coding

Use the priority stars on applicant cards to indicate urgency, not quality. One star = standard, two stars = urgent fill, three stars = critical role. Then use tags with colors for source tracking (green = referral, blue = website, orange = LinkedIn). This lets recruiters visually scan 50 cards and immediately spot urgent referrals that need attention.

06

Offer Letters, Employee Creation, and the Handoff to HR in Odoo 19

The final stretch of the recruitment pipeline is where most systems fall apart. The recruiter sends an offer in a Word doc, someone from HR manually creates the employee record two weeks later, and payroll discovers the new hire on their first day. Odoo 19 eliminates these handoff gaps.

Step 1: Generate Offer Letters from Templates

Configure offer templates in Recruitment → Configuration → Offer Templates. Each template uses Odoo's report engine with placeholders for salary, start date, job title, and department. When you move an applicant to the "Offer" stage, click "Generate Offer" to produce a PDF from the template.

XML — Offer letter report template (QWeb)
<template id="report_offer_letter">
  <t t-call="web.html_container">
    <t t-foreach="docs" t-as="o">
      <t t-call="web.external_layout">
        <div class="page">
          <h2>Offer of Employment</h2>
          <p>Dear <t t-out="o.partner_name"/>,</p>

          <p>
            We are pleased to offer you the position of
            <strong><t t-out="o.job_id.name"/></strong>
            in the <t t-out="o.department_id.name"/> department,
            reporting to <t t-out="o.department_id.manager_id.name"/>.
          </p>

          <p>
            <strong>Start Date:</strong>
            <t t-out="o.x_start_date" t-options="{'widget': 'date'}"/>
          </p>
          <p>
            <strong>Annual Salary:</strong>
            <t t-out="o.salary_proposed"
               t-options="{'widget': 'monetary',
                           'display_currency': o.company_id.currency_id}"/>
          </p>

          <p>
            This offer is contingent upon successful completion of
            background verification and reference checks.
          </p>

          <p>Please confirm your acceptance by
            <t t-out="o.x_offer_deadline"
               t-options="{'widget': 'date'}"/>.
          </p>
        </div>
      </t>
    </t>
  </t>
</template>

Step 2: Convert Applicant to Employee

When the candidate accepts, click "Create Employee" on the applicant record. Odoo 19 creates an hr.employee record and copies: name, email, phone, department, job position, job title, and the recruitment responsible as the employee's HR responsible. The applicant record links to the employee via the emp_id field for full audit trail.

Python — Extend employee creation to auto-trigger onboarding
class HrApplicant(models.Model):
    _inherit = 'hr.applicant'

    def create_employee_from_applicant(self):
        """Override to trigger onboarding plan after employee creation."""
        result = super().create_employee_from_applicant()

        for applicant in self:
            if applicant.emp_id:
                employee = applicant.emp_id
                # Assign default onboarding plan
                onboarding_plan = self.env.ref(
                    'my_module.default_onboarding_plan',
                    raise_if_not_found=False,
                )
                if onboarding_plan:
                    employee.sudo().write({
                        'plan_ids': [(4, onboarding_plan.id)],
                    })
                    # Launch onboarding activities
                    employee.sudo()._launch_plan(onboarding_plan)

        return result

Step 3: The HR Handoff Checklist

Use Odoo's Onboarding Plans (configured in Employees → Configuration → Plans) to auto-generate activities for the new hire's first week: IT setup (email, laptop, VPN), payroll enrollment (tax forms, bank details), manager welcome (1:1, team norms, buddy assignment), compliance (handbook, NDA), and system access (Odoo user account, security groups).

The Employee Record is the Source of Truth

Once the applicant converts to an employee, stop updating the applicant record. All subsequent data (contract, payroll, leaves, expenses) lives on the employee record. The applicant record becomes a historical artifact — useful for recruitment analytics but not for ongoing HR operations. We configure access rights so recruiters can read but not write to converted applicant records.

07

4 Recruitment Module Mistakes That Silently Sabotage Your Hiring Pipeline

1

Not Configuring Stage-Specific Email Templates

Odoo can send automated emails when an applicant moves between stages — but only if you assign an email template to each stage. Without this, candidates submit an application and hear nothing for two weeks. They assume you are not interested and accept another offer. Your pipeline shows 50 applicants in "Initial Screening" but half have already moved on.

Our Fix

At minimum, configure email templates for: Application Received (immediate acknowledgement), Interview Scheduled (with date/time/link), and Rejection (respectful, with timeline). The acknowledgement email alone reduces candidate drop-off by 30% in our experience.

2

Using a Single Pipeline for All Job Types

A senior engineering hire needs a coding challenge, system design review, and VP approval. A warehouse operator needs a skills test and a background check. If both go through the same 6-stage pipeline, warehouse applicants sit in "Technical Interview" and "Culture Fit" stages that don't apply to them, and recruiters waste time dragging cards past irrelevant columns.

Our Fix

Create department-specific stage sets. In Odoo 19, recruitment stages can be filtered by department using the department_ids many2many field on hr.recruitment.stage. Engineering sees 6 stages; Operations sees 4. Same module, different workflows.

3

Forgetting to Track Recruitment Sources

Without source tracking, you cannot answer the most important recruitment ROI question: "Which channel produces hires, and at what cost?" You spend $5,000/month on LinkedIn job slots and get 200 applications, but only 2 hires. Meanwhile, employee referrals produce 5 hires at zero ad spend. Without source data in Odoo, this insight is invisible.

Our Fix

Configure UTM sources for every channel: website, LinkedIn, Indeed, referrals, agencies, university fairs. Use source_id and medium_id on the applicant record. Add UTM parameters to external job post URLs so Odoo auto-captures the source on web applications. Run the Recruitment Analysis pivot report grouped by source and stage monthly.

4

No GDPR/Data Retention Policy for Applicant Records

Under GDPR (and similar regulations), you cannot keep applicant personal data indefinitely. A rejected candidate's resume, cover letter, and interview notes must be deleted or anonymized within a defined retention period — typically 6 to 24 months depending on jurisdiction. Odoo does not enforce this automatically. Your recruitment database grows forever, filled with personal data you have no legal basis to retain.

Our Fix

Create a scheduled action that runs monthly, finds refused/archived applicants older than your retention period, and anonymizes their records: replace partner_name with "Anonymized", clear email_from, and delete attached documents. Log the anonymization for audit compliance. We ship this as a standard module for EU-based clients.

BUSINESS ROI

What a Properly Configured Recruitment Pipeline Saves Your Business

Recruitment is one of the most expensive HR functions. Every day a position stays open costs the business in lost productivity, overtime for existing staff, and missed revenue. Here's what changes when you stop recruiting from spreadsheets:

40%Faster Time to Hire

Automated stage transitions, interview scheduling, and offer generation eliminate the manual delays that add 2-3 weeks to every hire.

60%Less Recruiter Admin Time

Email templates, auto-tagging, and pipeline automation free recruiters to focus on candidate relationships instead of data entry.

3xBetter Source ROI Visibility

UTM tracking and source-to-hire reporting shows exactly which channels produce quality hires — so you stop spending on channels that only produce volume.

The hidden ROI is candidate experience. Companies that respond within 24 hours and communicate at every stage win candidates over competitors who ghost them for two weeks. Odoo's automated templates and structured pipeline make this responsiveness the default.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to Odoo 19 Recruitment: configure job positions, publish careers pages, track applicants through Kanban, schedule interviews, send offers, and create employees.

H2 Keywords

1. "How to Configure Job Positions and Departments in Odoo 19 Recruitment" 2. "Applicant Tracking in Odoo 19" 3. "Mastering the Odoo 19 Recruitment Kanban" 4. "4 Recruitment Module Mistakes That Silently Sabotage Your Hiring Pipeline"

Stop Hiring from Spreadsheets and Shared Inboxes

Every resume that sits in a shared inbox for a week is a candidate lost to a competitor who replied in 24 hours. Every hiring decision based on gut feeling instead of structured interview scores is a lawsuit waiting to happen.

If you are running Odoo and still managing recruitment outside the system, we can help. We configure the full recruitment pipeline — job positions, careers pages, Kanban stages, interview surveys, offer templates, and the employee handoff — tailored to your hiring workflow.

Book a Free HR Module Assessment