MSP Contact Lifecycle
Last updated: 2026-10-01.
This page documents the implemented CRM Contact automations that keep MSP Seats aligned with onboarding, recurring, and offboarding contact states. Each seat is owned by an Account and linked to a Contact. The eligible Service authorises MSP enrolment but is not the seat's parent.
Contact Created
When a Contact is created, the automation tries to create or reuse an MSP Seat for the Contact if the Contact belongs to a company with exactly one active Service with MSP Seats Enabled checked.
Rules currently implemented:
- If the Contact has no company, no seat is created.
- If the Contact came from the onboarding form and has no company, a warning email is sent to the configured debug recipients so the Contact can be fixed.
- Eligibility requires the same Company, Service Status
Active, andMSP_Seats_Enabledsaved as true. Blank or false does not enrol contacts. - If the company has no eligible Service, no seat is created.
- If the company has more than one eligible Service, the automation stops and sends a manual-review email. It does not guess which plan is correct.
- If the Contact or active MSP plan is missing an ID, the automation stops for manual review.
- If more than one MSP Seat already exists for the same Contact and Account, the automation stops for manual review.
- If exactly one matching MSP Seat already exists, the automation reuses it
and still ensures the Contact lead status is
ONBOARDING. - If no matching MSP Seat exists, the automation creates one with:
Account: the Contact company, matching the eligible Service's Account;Contact: the created Contact;Billing_Status:Onboarding;Billing_Start: the ContactCreated_Timedate;Billing_End: blank.
- Whenever the automation can safely continue past the plan/seat checks, it
ensures the Contact
Lead_StatusisONBOARDING.
The retained Service module uses the Client_Services API. Its Plan_Status
field and MSP_Seats_Enabled flag retain the enrolment gate. Project and other
Services can be active without enabling MSP contact automation. New seats have
Account and Contact lookups; they do not write the legacy Seat Plan field.
Business reasoning:
- A new staff Contact for an MSP client should become billable/onboarding data only when the owning company and MSP entitlement are unambiguous.
- Onboarding starts as a seat lifecycle state, but the Contact also moves to
ONBOARDINGso existing CRM views and team workflows remain aligned. - Duplicate Account/Contact seats and multiple eligible Services require review, preserving the existing enrolment gate. A seat missing its Account also requires review before contact or seat changes. Automation leaves their resolution to a person.
Contact Edited
When a Contact is edited, the automation looks at update_source first, then
at Lead_Status.
Offboarding Form
If update_source is offboarding_form, the automation treats the edit as an
offboarding event.
Rules currently implemented:
- The same company / single active plan / single matching seat checks apply.
- If a corresponding MSP Seat exists, it is ended:
Billing_Status:Ended;Billing_End: today.
- If the seat is already
Endedand already has aBilling_End, the existing end date is preserved. - After the seat is ended or confirmed ended, the Contact is updated to:
Lead_Status:BUSINESS PREVIOUS;update_source: blank.
Recurring Contact Status
If Lead_Status is BUSINESS RECURRING or BUSINESS OWNER RECURRING, the
Contact should have an active MSP Seat.
Rules currently implemented:
- If no corresponding MSP Seat exists, one is created with
Billing_StatusActiveandBilling_Startset to today. - If a corresponding seat exists but is ended or onboarding, it is updated to:
Billing_Status:Active;Billing_End: blank.
- If the seat is already active with no end date, no seat change is needed.
Onboarding Contact Status
If Lead_Status is ONBOARDING, the Contact should have an onboarding MSP
Seat.
Rules currently implemented:
- If no corresponding MSP Seat exists, one is created with
Billing_StatusOnboardingandBilling_Startset to today. - If a corresponding seat exists, it is updated to
Billing_StatusOnboarding. Billing_Endis cleared when needed so the seat can participate in the current lifecycle again.
Business Previous Status
If Lead_Status is BUSINESS PREVIOUS, the Contact should stop counting as an
active MSP seat only when the current seat is active.
Rules currently implemented:
- If a corresponding seat is
Active, it is ended withBilling_StatusEndedandBilling_Endset to today. - If the corresponding seat is not active, the automation skips rather than forcing a different lifecycle transition.
- If no corresponding seat exists, the automation skips.
Other Edits
If the edit is not an offboarding form event and the Contact lead status is not one of the implemented MSP lifecycle statuses, no MSP Seat change is made.
Review and Skip Rules
The automation skips safely when:
- the Contact has no company;
- the company has no active MSP plan;
- the edit does not match an implemented MSP lifecycle case;
- a
BUSINESS PREVIOUSContact has no active corresponding seat to end.
The automation asks for manual review when:
- the company has multiple active MSP plans;
- the Contact or plan is missing an ID;
- more than one MSP Seat exists for the same Contact and Account;
- an existing seat that must be updated has no seat ID.
Manual-review and warning emails are sent to DEBUG_RECIPIENT_EMAILS through
Zoho Mail.
Deliberate Limits
- The Deluge functions do not contain business logic; they only pass the Contact ID to middleware.
- The automation does not choose between multiple active plans.
- The automation does not merge duplicate seats.
- The automation does not create or update Client Services.
References
- Technical route:
POST /zoho/contacts/on-create - Technical route:
POST /zoho/contacts/on-edit - Code:
app/automations/contact_created.py - Code:
app/automations/contact_edited.py - Deluge wrappers:
zoho_crm/deluge/standalone.on_create_contact_automations.dg,zoho_crm/deluge/standalone.on_edit_contact_automations.dg - Tests:
tests/test_contact_created_automation.py,tests/test_contact_edited_automation.py