EVERJUST Calendar + Contacts — Agent Skill
Operate the Calendar and Contacts app of a live everjust.app tenant as an agent:
create/find/update contacts, segment them with tags, and schedule/read calendar
events (with attendees and RSVP state), all through the Odoo MCP / ORM. This is
stock Odoo calendar + contacts (Odoo 19 CE fork) with a thin everjust layer —
not a bespoke app. Ground truth for this skill was read from the addon source under
<ww.everjust.app>/addons/ and introspected live on the
connectdomain tenant.
You reach every model through the platform's Odoo MCP tools (search, get, count,
find, create, update, delete, call, describe_model). See [[everjust-agent-mcp]]
for opening the connection and the exact tool signatures; see [[everjust-platform]] for
the tenancy model and the invariants (one DB per tenant, everything per-company_id) you
must not break. This skill is the "how to operate calendar + contacts correctly HERE".
When to use this skill
- Contacts (
res.partner) — create a person or company, look one up by name/email,
update details, set a company↔contact parent link, tag/segment with categories, or
read a partner before you mail/invoice/schedule them.
- Calendar (
calendar.event) — schedule a meeting, reschedule or cancel it, add/read
attendees and their RSVP (accepted/declined/tentative/needsAction), set an event
location / videocall link, or check a user's free/busy to avoid a double-booking.
- Understanding a booking → event — an
everjust_appointment booking auto-creates a
calendar.event; use this skill to read/adjust the resulting event.
Do NOT use this skill for:
- Sending email as a tenant mailbox, mailbox triage, or send-gating — that is the
custom
everjust.mail.* stack; use [[everjust-mail-ops]]. res.partner is only the
recipient/author spine there.
- Creating the appointment booking flow end-to-end (types, slots, public page) —
that is the
everjust_appointment module's own surface; this skill covers the
calendar.event it emits, and the crm.lead it can spawn.
- Registrar DNS / product work ([[godaddy-api]], the connectdomain app) — unrelated.
Architecture — the model map
One tenant DB per customer. Everything is per-company_id. res.partner is the single
shared contact record used by CRM (crm.lead.partner_id), mail (author/recipient),
projects, invoicing (account.move.partner_id), telephony, and appointments — there is
no separate "calendar contact" or "CRM contact". Editing a partner here changes it
everywhere.
| Model |
_name |
Role |
Notable real fields (introspected) |
| Contact |
res.partner |
The shared person/company spine. |
name (REQ), email, phone, is_company, company_type (computed person/company), parent_id (→ the company), child_ids, category_id (many2many → res.partner.category — the tags), function (job title), street/city/country_id, vat, ref, lang, tz, comment (html), company_id, type (contact/invoice/delivery/other), user_ids (→ the login, if this partner is a user), is_agent (everjust) |
| Tag |
res.partner.category |
Contact segmentation tags (Odoo "Contact Tags"). Hierarchical. |
name (REQ), parent_id, child_ids, color, active, partner_ids |
| Event |
calendar.event |
A calendar entry / meeting. |
name (REQ), start (datetime REQ), stop (datetime REQ), allday, start_date/stop_date (used when allday), duration (float hours), user_id (organizer → res.users), partner_ids (many2many → res.partner — set this to invite; attendees are derived), attendee_ids (one2many → calendar.attendee, auto-managed), location, videocall_location, description (html), privacy (public/private/confidential), show_as (free/busy, REQ), recurrency+rrule+recurrence_id, alarm_ids (reminders), categ_ids (→ calendar.event.type), res_model/res_id (link back to a source record), google_id/need_sync/active (sync/soft-delete) |
| Attendee |
calendar.attendee |
One partner's participation + RSVP in an event. Auto-created from partner_ids. |
event_id (REQ), partner_id (REQ), email (computed from partner), common_name, state (accepted/declined/tentative/needsAction), availability (free/busy), access_token |
The everjust layer on this app (what's non-stock)
The core is stock Odoo. The everjust customizations that matter to an operating agent:
res.partner.is_agent (everjust_mail) — a boolean marking a partner as an AI
agent actor (so it's badged, never mistaken for a teammate). Set only by the merge/agent
program — see [[everjust-agent-mcp]]. Don't set it on human contacts.
res.partner action methods — action_phone_call / action_phone_sms
(everjust_phone) and action_ringover_call (everjust_ringover) open a softphone/SMS
composer from a contact. These are UI client actions (return an ir.actions.client);
they do NOT place a call or send an SMS from the ORM, and both modules are uninstalled
on connectdomain anyway. Don't call them expecting a message to go out.
everjust_appointment — a custom module (replacing the Enterprise appointment app)
whose appointment.booking.action_confirm() creates a calendar.event via sudo()
with the customer + staff as partner_ids, and optionally spawns a crm.lead. So some
calendar.event rows originate from bookings; booking.calendar_event_id links them and
booking.action_cancel() unlink()s the event.
Calendar sync (Google / Microsoft)
Sync is stock Odoo and per-user, gated by two layers:
google_calendar is INSTALLED on connectdomain and the platform OAuth app is
configured (ir.config_parameter google_calendar_client_id / _client_secret are set).
Sync tokens live per user on res.users: google_calendar_rtoken,
google_calendar_token, google_calendar_token_validity, google_calendar_sync_token,
google_calendar_cal_id. A user only syncs once they've clicked "Sync with Google" and
authorized — the config params existing does NOT mean any user is connected.
microsoft_calendar is NOT installed on connectdomain; there is no Outlook sync
here. Do not assume it.
- On a synced event,
google_id is the remote id and need_sync=True marks a local change
the cron will push. Deleting/moving a synced event propagates to the user's real Google
calendar — treat writes to synced events as writes to their live personal calendar.
Recipes
Tool calls below use the MCP tool names from [[everjust-agent-mcp]] (search, get,
find, create, update, call, describe_model; in Claude Code they're namespaced
mcp__everjust__<tool>). Domains are Odoo triples; true/false are Odoo booleans.
describe_model the model and check your_access before any write — never guess field
names.
1. Find or read a contact (before you act on it)
# Resolve a name → id (fuzzy, matches name/email/ref)
find(model="res.partner", name="Jane Doe")
→ [[412, "Jane Doe"], [897, "Jane Doe, Acme Corp"]]
# Read the useful fields
get(model="res.partner", ids=[412],
fields=["name","email","phone","function","parent_id","company_type",
"category_id","user_ids","is_agent","company_id"])
# Search a segment: all companies tagged "Custom-Domain ICP" with an email
search(model="res.partner",
domain=[["is_company","=",true],
["category_id.name","=","Custom-Domain ICP"],
["email","!=",false]],
fields=["name","email","category_id"], limit=50, order="name")
Always resolve to an id and read the partner before emailing ([[everjust-mail-ops]]),
invoicing, or scheduling — the same record backs all of them, so you want the right one.
Note the tenant has ~1,500 partners and ~24 category tags (e.g. Customer, Agency,
Cold Outreach, Custom-Domain ICP).
2. Create a contact, and link a person to their company
# Company first (is_company=true → company_type computes to "company")
create(model="res.partner", values={
"name": "Acme Corp", "is_company": true,
"email": "hello@acme.com", "phone": "+1 612 555 0100",
"street": "100 Main St", "city": "Minneapolis"})
→ { created_id: 1601 }
# Person linked to that company via parent_id (function = their job title)
create(model="res.partner", values={
"name": "Jane Doe", "is_company": false, "parent_id": 1601,
"email": "jane@acme.com", "function": "Head of Ops"})
→ { created_id: 1602 }
There is no mobile and no title field on this tenant's res.partner (Odoo 19
merged mobile into phone; salutation title isn't exposed). Use phone and function.
Set parent_id (not a free-text company name) so the person rolls up to the company across
CRM/invoicing. Do not set is_agent — that's reserved for AI-agent partners.
3. Tag / segment a contact with categories (category_id)
category_id is many2many, so use Odoo command tuples — not a bare id.
# Resolve tag ids
find(model="res.partner.category", name="Customer") → [[20, "Customer"]]
find(model="res.partner.category", name="Custom-Domain ICP") → [[22, "Custom-Domain ICP"]]
# ADD tags without clobbering existing ones: command (4, id)
update(model="res.partner", ids=[1602], values={"category_id": [[4, 20], [4, 22]]})
# REPLACE the whole tag set: command (6, 0, [ids])
update(model="res.partner", ids=[1602], values={"category_id": [[6, 0, [20]]]})
# Create a new tag if it doesn't exist
create(model="res.partner.category", values={"name": "Q3 Webinar", "color": 4})
Use [[4, id]] to add and [[3, id]] to remove a single tag; reserve [[6,0,[...]]]
(replace-all) for when you truly mean to overwrite every tag on the contact.
4. Schedule a calendar event with attendees
Set partner_ids to invite people — Odoo auto-creates the calendar.attendee rows
(each starting at state="needsAction"). Don't hand-craft attendee_ids.
# Organizer = a res.users id; resolve attendees as res.partner ids first
find(model="res.users", name="Sam Staff") → [[7, "Sam Staff"]]
find(model="res.partner", name="Jane Doe") → [[1602, "Jane Doe"]]
create(model="calendar.event", values={
"name": "Onboarding call — Acme",
"start": "2026-07-10 15:00:00", # UTC, 'YYYY-MM-DD HH:MM:SS'
"stop": "2026-07-10 15:30:00",
"user_id": 7, # organizer (res.users)
"partner_ids": [[6, 0, [1602, <sam_partner_id>]]], # invitees → attendees auto-made
"location": "Zoom",
"videocall_location": "https://zoom.us/j/123",
"show_as": "busy",
"description": "<p>Kickoff.</p>"})
→ { created_id: 55 }
# All-day event: set allday + start_date/stop_date instead of start/stop times
create(model="calendar.event", values={
"name": "Holiday", "allday": true,
"start_date": "2026-07-04", "stop_date": "2026-07-04", "show_as": "free"})
Datetimes are stored UTC; a user sees them in their tz. If user_id (the organizer)
has authorized Google sync, this event will push to their real Google Calendar
(need_sync/google_id) — see Pitfall 5.
5. Read attendees / RSVP, and reschedule or cancel
# Who's invited and did they respond?
search(model="calendar.attendee",
domain=[["event_id","=",55]],
fields=["partner_id","email","state","availability"])
→ state ∈ {accepted, declined, tentative, needsAction}
# Reschedule (moves it on any synced Google calendar too)
update(model="calendar.event", ids=[55],
values={"start":"2026-07-10 16:00:00","stop":"2026-07-10 16:30:00"})
# Add another invitee later (command (4, partner_id) → new attendee auto-created)
update(model="calendar.event", ids=[55], values={"partner_ids": [[4, 1601]]})
# Cancel = archive (soft delete) so sync + history behave; prefer over hard unlink
update(model="calendar.event", ids=[55], values={"active": false})
Set an attendee's own RSVP through the event's response methods rather than writing
state raw when possible, e.g.
call(model="calendar.event", method="action_open_composer" ...) is UI-only —
for a plain status set, update(model="calendar.attendee", ids=[id], values={"state":"accepted"})
is acceptable but won't notify. Reserve hard delete(confirm=true) for events you truly
want gone; for synced events, deleting removes them from the user's Google calendar.
6. Check free/busy to avoid a double-booking
The everjust_appointment module already does this (_check_staff_availability); mirror
its query to check any organizer's calendar before you schedule:
count(model="calendar.event",
domain=[["user_id","=",7],
["start","<","2026-07-10 15:30:00"],
["stop", ">","2026-07-10 15:00:00"],
["active","=",true]])
→ 0 ⇢ the slot is free for that user
A non-zero count means an overlapping event exists — pick another time or organizer. If the
task is really "book an appointment through the customer-facing flow", drive
appointment.booking (create → action_confirm) instead, and it will create the
calendar.event (and optionally a crm.lead) for you.
Pitfalls
res.partner is shared — edits ripple everywhere. The same row is the CRM contact,
the mail recipient/author, the invoice partner, the project follower, and the calendar
attendee. Renaming, re-emailing, merging, or archiving a partner changes it across every
app. Confirm you have the right partner (find → get) before mutating, and prefer
adding data over overwriting.
category_id is many2many — use command tuples, not a bare id. Writing
category_id: 20 fails or misbehaves; use [[4,20]] to add, [[3,20]] to remove,
[[6,0,[...]]] to replace all. Same rule for partner_ids on events. (General x2many
rule from [[everjust-agent-mcp]], but easy to trip on here.)
Invite via partner_ids, never by writing attendee_ids directly. Odoo derives and
manages calendar.attendee from partner_ids. Hand-inserting attendee rows desyncs the
two, breaks RSVP, and breaks Google sync. New attendees start at state="needsAction",
and attendee state uses the Google vocabulary (needsAction, not pending).
No mobile, no title on this tenant's res.partner. Odoo 19 merged mobile into
phone and the salutation title field isn't exposed. Don't create/update those keys —
it's a hard error. Use phone and function (job title).
Writing a synced event writes the user's real Google calendar. google_calendar is
installed and OAuth is configured on connectdomain, and sync is per-user (tokens on
res.users). If the organizer has connected Google, creating/moving/deleting the event
propagates to their live personal calendar via need_sync/google_id. Treat such writes
as live-calendar writes. microsoft_calendar is not installed — there is no Outlook
sync here; don't assume it.
Config params set ≠ a user is synced. google_calendar_client_id/_client_secret
being present is the platform OAuth app; a given user only syncs after authorizing (has
google_calendar_rtoken on their res.users). Check the organizer's user record before
assuming their events reach Google.
is_agent is reserved. It marks an AI-agent partner (badged, non-human) and is set by
the merge/agent program — see [[everjust-agent-mcp]]. Never set it on a human contact, and
don't infer "this is a bot" from anything else.
The res.partner phone/SMS/Ringover action methods don't send anything. action_phone_call,
action_phone_sms, action_ringover_call return ir.actions.client for the web UI to
open a dialer — they place no call and send no SMS from the ORM. Both everjust_phone and
everjust_ringover are uninstalled on connectdomain, so those methods aren't even
present here. For SMS, the tenant has everjust_sms_gateway installed (a separate
send path), not these contact actions.
Cancel by archiving, not (usually) hard delete. Set active=false to cancel an event
so history and sync reconcile cleanly; a hard delete(confirm=true) is irreversible and,
for synced events, removes it from the user's Google calendar. For appointment-originated
events, cancel the booking (appointment.booking.action_cancel) — it unlink()s its
linked calendar_event_id for you.
Everything is per-company_id / one tenant per connection. Categories, partners, and
events are tenant-scoped; the MCP Bearer key binds you to one workspace. Confirm you're on
the right tenant before searching or writing (see [[everjust-platform]] / [[everjust-agent-mcp]]).
See also
- [[everjust-platform]] — tenancy model, Odoo-19 shape, and the invariants (one DB per
tenant, per-
company_id) behind everything here.
- [[everjust-agent-mcp]] — how to connect and the exact
search/get/create/update/
call tool signatures and x2many command-tuple rules used in every recipe above.
- [[everjust-mail-ops]] — sending/reading mail as a tenant mailbox.
res.partner is only the
recipient/author spine there; the actual send stack is the custom everjust.mail.* app.
everjust_appointment (module) — the booking → calendar.event (+ optional crm.lead)
flow; drive appointment.booking when the task is a customer-facing booking.
1---2name: everjust-calendar-contacts3description: Operate the Calendar + Contacts app of an everjust.app tenant over the Odoo MCP/ORM — the shared res.partner contact spine and calendar.event scheduling, with optional Google/Microsoft calendar sync. Use when the task is to create/find/update a contact (person or company), tag/segment contacts with res.partner.category, look up who a partner is before mailing or invoicing them, schedule/reschedule/cancel a calendar.event, add or read attendees and their RSVP state, check a user's free/busy for double-booking, or reason about how a booking became a calendar event. This is STOCK Odoo calendar + contacts with a thin everjust layer (res.partner.is_agent flag, phone/SMS action methods, and everjust_appointment writing events) — NOT a custom webmail or a bespoke scheduler. res.partner is the ONE contact record shared across CRM, mail, projects, invoicing and appointments; edits here ripple everywhere. Cross-references [[everjust-platform]], [[everjust-agent-mcp]], and [[everjust-mail-ops]].4---56# EVERJUST Calendar + Contacts — Agent Skill78Operate the **Calendar and Contacts** app of a live everjust.app tenant as an agent:9create/find/update contacts, segment them with tags, and schedule/read calendar10events (with attendees and RSVP state), all through the Odoo MCP / ORM. This is11**stock Odoo `calendar` + `contacts`** (Odoo 19 CE fork) with a **thin everjust layer** —12not a bespoke app. Ground truth for this skill was read from the addon source under13`<ww.everjust.app>/addons/` and introspected live on the14`connectdomain` tenant.1516You reach every model through the platform's Odoo MCP tools (`search`, `get`, `count`,17`find`, `create`, `update`, `delete`, `call`, `describe_model`). See [[everjust-agent-mcp]]18for opening the connection and the exact tool signatures; see [[everjust-platform]] for19the tenancy model and the invariants (one DB per tenant, everything per-`company_id`) you20must not break. This skill is the "how to operate calendar + contacts correctly HERE".2122## When to use this skill2324- **Contacts (`res.partner`)** — create a person or company, look one up by name/email,25 update details, set a company↔contact parent link, tag/segment with categories, or26 read a partner before you mail/invoice/schedule them.27- **Calendar (`calendar.event`)** — schedule a meeting, reschedule or cancel it, add/read28 attendees and their RSVP (`accepted`/`declined`/`tentative`/`needsAction`), set an event29 location / videocall link, or check a user's free/busy to avoid a double-booking.30- **Understanding a booking → event** — an `everjust_appointment` booking auto-creates a31 `calendar.event`; use this skill to read/adjust the resulting event.3233**Do NOT use this skill for:**34- **Sending email** as a tenant mailbox, mailbox triage, or send-gating — that is the35 custom `everjust.mail.*` stack; use [[everjust-mail-ops]]. `res.partner` is only the36 *recipient/author* spine there.37- **Creating the appointment booking flow** end-to-end (types, slots, public page) —38 that is the `everjust_appointment` module's own surface; this skill covers the39 `calendar.event` it emits, and the `crm.lead` it can spawn.40- **Registrar DNS / product** work ([[godaddy-api]], the connectdomain app) — unrelated.4142## Architecture — the model map4344One tenant DB per customer. Everything is per-`company_id`. `res.partner` is the **single45shared contact record** used by CRM (`crm.lead.partner_id`), mail (author/recipient),46projects, invoicing (`account.move.partner_id`), telephony, and appointments — there is47no separate "calendar contact" or "CRM contact". **Editing a partner here changes it48everywhere.**4950| Model | `_name` | Role | Notable real fields (introspected) |51|---|---|---|---|52| **Contact** | `res.partner` | The shared person/company spine. | `name` (REQ), `email`, `phone`, `is_company`, `company_type` (computed `person`/`company`), `parent_id` (→ the company), `child_ids`, `category_id` (**many2many → `res.partner.category`** — the tags), `function` (job title), `street`/`city`/`country_id`, `vat`, `ref`, `lang`, `tz`, `comment` (html), `company_id`, `type` (`contact`/`invoice`/`delivery`/`other`), `user_ids` (→ the login, if this partner is a user), **`is_agent`** (everjust) |53| **Tag** | `res.partner.category` | Contact segmentation tags (Odoo "Contact Tags"). Hierarchical. | `name` (REQ), `parent_id`, `child_ids`, `color`, `active`, `partner_ids` |54| **Event** | `calendar.event` | A calendar entry / meeting. | `name` (REQ), `start` (datetime REQ), `stop` (datetime REQ), `allday`, `start_date`/`stop_date` (used when `allday`), `duration` (float hours), `user_id` (organizer → `res.users`), **`partner_ids`** (many2many → `res.partner` — set this to invite; attendees are derived), `attendee_ids` (one2many → `calendar.attendee`, auto-managed), `location`, `videocall_location`, `description` (html), `privacy` (`public`/`private`/`confidential`), `show_as` (`free`/`busy`, REQ), `recurrency`+`rrule`+`recurrence_id`, `alarm_ids` (reminders), `categ_ids` (→ `calendar.event.type`), `res_model`/`res_id` (link back to a source record), `google_id`/`need_sync`/`active` (sync/soft-delete) |55| **Attendee** | `calendar.attendee` | One partner's participation + RSVP in an event. Auto-created from `partner_ids`. | `event_id` (REQ), `partner_id` (REQ), `email` (computed from partner), `common_name`, `state` (`accepted`/`declined`/`tentative`/`needsAction`), `availability` (`free`/`busy`), `access_token` |5657### The everjust layer on this app (what's non-stock)5859The core is stock Odoo. The everjust customizations that matter to an operating agent:60611. **`res.partner.is_agent`** (`everjust_mail`) — a boolean marking a partner as an AI62 agent actor (so it's badged, never mistaken for a teammate). Set only by the merge/agent63 program — see [[everjust-agent-mcp]]. Don't set it on human contacts.642. **`res.partner` action methods** — `action_phone_call` / `action_phone_sms`65 (`everjust_phone`) and `action_ringover_call` (`everjust_ringover`) open a softphone/SMS66 composer from a contact. These are **UI client actions** (return an `ir.actions.client`);67 they do NOT place a call or send an SMS from the ORM, and both modules are **uninstalled68 on `connectdomain`** anyway. Don't `call` them expecting a message to go out.693. **`everjust_appointment`** — a custom module (replacing the Enterprise appointment app)70 whose `appointment.booking.action_confirm()` **creates a `calendar.event` via `sudo()`**71 with the customer + staff as `partner_ids`, and optionally spawns a `crm.lead`. So some72 `calendar.event` rows originate from bookings; `booking.calendar_event_id` links them and73 `booking.action_cancel()` `unlink()`s the event.7475### Calendar sync (Google / Microsoft)7677Sync is **stock Odoo** and **per-user**, gated by two layers:7879- **`google_calendar`** is INSTALLED on `connectdomain` and the platform OAuth app is80 configured (`ir.config_parameter` `google_calendar_client_id` / `_client_secret` are set).81 Sync tokens live **per user** on `res.users`: `google_calendar_rtoken`,82 `google_calendar_token`, `google_calendar_token_validity`, `google_calendar_sync_token`,83 `google_calendar_cal_id`. A user only syncs once they've clicked "Sync with Google" and84 authorized — the config params existing does NOT mean any user is connected.85- **`microsoft_calendar`** is **NOT installed** on `connectdomain`; there is no Outlook sync86 here. Do not assume it.87- On a synced event, `google_id` is the remote id and `need_sync=True` marks a local change88 the cron will push. Deleting/moving a synced event **propagates to the user's real Google89 calendar** — treat writes to synced events as writes to their live personal calendar.9091---9293## Recipes9495Tool calls below use the MCP tool names from [[everjust-agent-mcp]] (`search`, `get`,96`find`, `create`, `update`, `call`, `describe_model`; in Claude Code they're namespaced97`mcp__everjust__<tool>`). Domains are Odoo triples; `true`/`false` are Odoo booleans.98**`describe_model` the model and check `your_access` before any write** — never guess field99names.100101### 1. Find or read a contact (before you act on it)102103```text104# Resolve a name → id (fuzzy, matches name/email/ref)105find(model="res.partner", name="Jane Doe")106 → [[412, "Jane Doe"], [897, "Jane Doe, Acme Corp"]]107108# Read the useful fields109get(model="res.partner", ids=[412],110 fields=["name","email","phone","function","parent_id","company_type",111 "category_id","user_ids","is_agent","company_id"])112113# Search a segment: all companies tagged "Custom-Domain ICP" with an email114search(model="res.partner",115 domain=[["is_company","=",true],116 ["category_id.name","=","Custom-Domain ICP"],117 ["email","!=",false]],118 fields=["name","email","category_id"], limit=50, order="name")119```120Always resolve to an id and read the partner **before** emailing ([[everjust-mail-ops]]),121invoicing, or scheduling — the same record backs all of them, so you want the right one.122Note the tenant has ~1,500 partners and ~24 category tags (e.g. Customer, Agency,123Cold Outreach, Custom-Domain ICP).124125### 2. Create a contact, and link a person to their company126127```text128# Company first (is_company=true → company_type computes to "company")129create(model="res.partner", values={130 "name": "Acme Corp", "is_company": true,131 "email": "hello@acme.com", "phone": "+1 612 555 0100",132 "street": "100 Main St", "city": "Minneapolis"})133 → { created_id: 1601 }134135# Person linked to that company via parent_id (function = their job title)136create(model="res.partner", values={137 "name": "Jane Doe", "is_company": false, "parent_id": 1601,138 "email": "jane@acme.com", "function": "Head of Ops"})139 → { created_id: 1602 }140```141There is **no `mobile` and no `title` field** on this tenant's `res.partner` (Odoo 19142merged mobile into `phone`; salutation title isn't exposed). Use `phone` and `function`.143Set `parent_id` (not a free-text company name) so the person rolls up to the company across144CRM/invoicing. Do **not** set `is_agent` — that's reserved for AI-agent partners.145146### 3. Tag / segment a contact with categories (`category_id`)147148`category_id` is **many2many**, so use Odoo command tuples — not a bare id.149150```text151# Resolve tag ids152find(model="res.partner.category", name="Customer") → [[20, "Customer"]]153find(model="res.partner.category", name="Custom-Domain ICP") → [[22, "Custom-Domain ICP"]]154155# ADD tags without clobbering existing ones: command (4, id)156update(model="res.partner", ids=[1602], values={"category_id": [[4, 20], [4, 22]]})157158# REPLACE the whole tag set: command (6, 0, [ids])159update(model="res.partner", ids=[1602], values={"category_id": [[6, 0, [20]]]})160161# Create a new tag if it doesn't exist162create(model="res.partner.category", values={"name": "Q3 Webinar", "color": 4})163```164Use `[[4, id]]` to add and `[[3, id]]` to remove a single tag; reserve `[[6,0,[...]]]`165(replace-all) for when you truly mean to overwrite every tag on the contact.166167### 4. Schedule a calendar event with attendees168169Set **`partner_ids`** to invite people — Odoo auto-creates the `calendar.attendee` rows170(each starting at `state="needsAction"`). Don't hand-craft `attendee_ids`.171172```text173# Organizer = a res.users id; resolve attendees as res.partner ids first174find(model="res.users", name="Sam Staff") → [[7, "Sam Staff"]]175find(model="res.partner", name="Jane Doe") → [[1602, "Jane Doe"]]176177create(model="calendar.event", values={178 "name": "Onboarding call — Acme",179 "start": "2026-07-10 15:00:00", # UTC, 'YYYY-MM-DD HH:MM:SS'180 "stop": "2026-07-10 15:30:00",181 "user_id": 7, # organizer (res.users)182 "partner_ids": [[6, 0, [1602, <sam_partner_id>]]], # invitees → attendees auto-made183 "location": "Zoom",184 "videocall_location": "https://zoom.us/j/123",185 "show_as": "busy",186 "description": "<p>Kickoff.</p>"})187 → { created_id: 55 }188189# All-day event: set allday + start_date/stop_date instead of start/stop times190create(model="calendar.event", values={191 "name": "Holiday", "allday": true,192 "start_date": "2026-07-04", "stop_date": "2026-07-04", "show_as": "free"})193```194Datetimes are stored **UTC**; a user sees them in their `tz`. If `user_id` (the organizer)195has authorized Google sync, this event will push to their real Google Calendar196(`need_sync`/`google_id`) — see Pitfall 5.197198### 5. Read attendees / RSVP, and reschedule or cancel199200```text201# Who's invited and did they respond?202search(model="calendar.attendee",203 domain=[["event_id","=",55]],204 fields=["partner_id","email","state","availability"])205 → state ∈ {accepted, declined, tentative, needsAction}206207# Reschedule (moves it on any synced Google calendar too)208update(model="calendar.event", ids=[55],209 values={"start":"2026-07-10 16:00:00","stop":"2026-07-10 16:30:00"})210211# Add another invitee later (command (4, partner_id) → new attendee auto-created)212update(model="calendar.event", ids=[55], values={"partner_ids": [[4, 1601]]})213214# Cancel = archive (soft delete) so sync + history behave; prefer over hard unlink215update(model="calendar.event", ids=[55], values={"active": false})216```217Set an attendee's own RSVP through the event's response methods rather than writing218`state` raw when possible, e.g.219`call(model="calendar.event", method="action_open_composer" ...)` is UI-only —220for a plain status set, `update(model="calendar.attendee", ids=[id], values={"state":"accepted"})`221is acceptable but won't notify. Reserve hard `delete(confirm=true)` for events you truly222want gone; for synced events, deleting removes them from the user's Google calendar.223224### 6. Check free/busy to avoid a double-booking225226The `everjust_appointment` module already does this (`_check_staff_availability`); mirror227its query to check any organizer's calendar before you schedule:228229```text230count(model="calendar.event",231 domain=[["user_id","=",7],232 ["start","<","2026-07-10 15:30:00"],233 ["stop", ">","2026-07-10 15:00:00"],234 ["active","=",true]])235 → 0 ⇢ the slot is free for that user236```237A non-zero count means an overlapping event exists — pick another time or organizer. If the238task is really "book an appointment through the customer-facing flow", drive239`appointment.booking` (create → `action_confirm`) instead, and it will create the240`calendar.event` (and optionally a `crm.lead`) for you.241242---243244## Pitfalls2452461. **`res.partner` is shared — edits ripple everywhere.** The same row is the CRM contact,247 the mail recipient/author, the invoice partner, the project follower, and the calendar248 attendee. Renaming, re-emailing, merging, or archiving a partner changes it across every249 app. Confirm you have the *right* partner (`find` → `get`) before mutating, and prefer250 adding data over overwriting.2512522. **`category_id` is many2many — use command tuples, not a bare id.** Writing253 `category_id: 20` fails or misbehaves; use `[[4,20]]` to add, `[[3,20]]` to remove,254 `[[6,0,[...]]]` to replace all. Same rule for `partner_ids` on events. (General x2many255 rule from [[everjust-agent-mcp]], but easy to trip on here.)2562573. **Invite via `partner_ids`, never by writing `attendee_ids` directly.** Odoo derives and258 manages `calendar.attendee` from `partner_ids`. Hand-inserting attendee rows desyncs the259 two, breaks RSVP, and breaks Google sync. New attendees start at `state="needsAction"`,260 and attendee `state` uses the Google vocabulary (`needsAction`, not `pending`).2612624. **No `mobile`, no `title` on this tenant's `res.partner`.** Odoo 19 merged mobile into263 `phone` and the salutation `title` field isn't exposed. Don't create/update those keys —264 it's a hard error. Use `phone` and `function` (job title).2652665. **Writing a synced event writes the user's real Google calendar.** `google_calendar` is267 installed and OAuth is configured on `connectdomain`, and sync is **per-user** (tokens on268 `res.users`). If the organizer has connected Google, creating/moving/deleting the event269 propagates to their live personal calendar via `need_sync`/`google_id`. Treat such writes270 as live-calendar writes. `microsoft_calendar` is **not installed** — there is no Outlook271 sync here; don't assume it.2722736. **Config params set ≠ a user is synced.** `google_calendar_client_id`/`_client_secret`274 being present is the *platform* OAuth app; a given user only syncs after authorizing (has275 `google_calendar_rtoken` on their `res.users`). Check the organizer's user record before276 assuming their events reach Google.2772787. **`is_agent` is reserved.** It marks an AI-agent partner (badged, non-human) and is set by279 the merge/agent program — see [[everjust-agent-mcp]]. Never set it on a human contact, and280 don't infer "this is a bot" from anything else.2812828. **The `res.partner` phone/SMS/Ringover action methods don't send anything.** `action_phone_call`,283 `action_phone_sms`, `action_ringover_call` return `ir.actions.client` for the web UI to284 open a dialer — they place no call and send no SMS from the ORM. Both `everjust_phone` and285 `everjust_ringover` are **uninstalled on `connectdomain`**, so those methods aren't even286 present here. For SMS, the tenant has `everjust_sms_gateway` installed (a separate287 send path), not these contact actions.2882899. **Cancel by archiving, not (usually) hard delete.** Set `active=false` to cancel an event290 so history and sync reconcile cleanly; a hard `delete(confirm=true)` is irreversible and,291 for synced events, removes it from the user's Google calendar. For appointment-originated292 events, cancel the **booking** (`appointment.booking.action_cancel`) — it `unlink()`s its293 linked `calendar_event_id` for you.29429510. **Everything is per-`company_id` / one tenant per connection.** Categories, partners, and296 events are tenant-scoped; the MCP Bearer key binds you to one workspace. Confirm you're on297 the right tenant before searching or writing (see [[everjust-platform]] / [[everjust-agent-mcp]]).298299## See also300301- [[everjust-platform]] — tenancy model, Odoo-19 shape, and the invariants (one DB per302 tenant, per-`company_id`) behind everything here.303- [[everjust-agent-mcp]] — how to connect and the exact `search`/`get`/`create`/`update`/304 `call` tool signatures and x2many command-tuple rules used in every recipe above.305- [[everjust-mail-ops]] — sending/reading mail as a tenant mailbox. `res.partner` is only the306 recipient/author spine there; the actual send stack is the custom `everjust.mail.*` app.307- `everjust_appointment` (module) — the booking → `calendar.event` (+ optional `crm.lead`)308 flow; drive `appointment.booking` when the task is a customer-facing booking.