FluentCRM: Companies model and contact-company relations
SkillAI & modelsWork with FluentCRM 3.x Companies / account records from companion plugins. Covers the experimental company_module flag, FluentCrmApi('companies'), Company model fields, createOrUpdate, owner_id as Subscriber ID, custom company fields in meta.custom_values, primary company vs many-to-many company membership, attachContactsByIds / detachContactsByIds, company hooks, and company-aware contact filters. Use when a plugin must sync organizations/accounts, attach contacts to companies, read company segments, add automation company actions, or audit code touching fc_companies, company_id, attachCompanies, or fluent_crm/company_ hooks.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the FluentCRM: Companies model and contact-company relations skill
What this skill tells your AI
The instructions your AI receives, as published by lonsdale201/wp-agent-skills in fluentcrm/fluentcrm-companies-model/SKILL.md and read by ahel’s review.
Use this skill when a plugin needs to sync B2B account/company records into FluentCRM or attach contacts to existing companies. Companies are core FluentCRM 3.1.13 code, but the UI/automation surface is behind the experimental company_module setting.
Guard the feature
For UI, automation actions, or user-visible sync flows, check the module flag:
use FluentCrm\App\Services\Helper;
if (!function_exists('FluentCrmApi') || !Helper::isCompanyEnabled()) {
return;
}
Helper::isCompanyEnabled() reads _fluentcrm_experimental_settings['company_module'] === 'yes'. The low-level API does not hard-block writes when the flag is off, so do not create surprise company data from a hidden integration unless the admin opted in.
API entry points
app/Api/config.php registers the API key as companies:
$companiesApi = FluentCrmApi('companies');
Create or update a company:
$company = FluentCrmApi('companies')->createOrUpdate([
'name' => sanitize_text_field($accountName),
'email' => sanitize_email($billingEmail),
'website' => esc_url_raw($website),
'type' => 'Customer',
'industry' => sanitize_text_field($industry),
'owner_id' => (int) $fluentContactId, // Subscriber ID, not WP user ID
'custom_values' => [
'external_account_id' => sanitize_text_field($externalId),
],
]);
Important behavior from Companies::createOrUpdate():
- Existing company lookup is by
idwhen provided, otherwise by exactname. owner_idis a FluentCRMSubscriberID. It is not a WordPress user ID.- Setting
owner_idattaches that contact to the company and sets the contact's primarycompany_idif empty. custom_valuesare formatted byCustomCompanyFieldand stored under serializedCompany.meta['custom_values'].- Create fires
fluent_crm/company_created; update firesfluent_crm/company_updated.
Do not call FluentCrmApi('companies')->getCompany($name) for name lookup. The method name says $idOrName, but the 3.1.13 source checks id for numeric values and email for strings. For name lookup use:
use FluentCrm\App\Models\Company;
$company = Company::where('name', $name)->first();
Data model
Company maps fc_companies. Core fields include:
name, owner_id, industry, type, email, phone, website,
address_line_1, address_line_2, postal_code, city, state, country,
timezone, employees_number, description, logo,
linkedin_url, facebook_url, twitter_url, date_of_start, meta
Relations:
Company::subscribers()usesfc_subscriber_pivotwithobject_type = FluentCrm\App\Models\Company.Company::owner()belongs to aSubscriberthroughowner_id.Company::notes()stores company notes infc_subscriber_noteswith status_company_note_.Subscriber::company()points to the primary company viafc_subscribers.company_id.Subscriber::companies()is the many-to-many relation through the pivot table.
Treat company_id as the primary/display company only. A contact can belong to multiple companies through Subscriber::companies().
Attach and detach contacts
Prefer the API wrapper for bulk changes:
$result = FluentCrmApi('companies')->attachContactsByIds(
[(int) $contactId],
[(int) $companyId]
);
if (!$result) {
// At least one company ID was invalid, no subscribers were found, or input was empty.
}
attachContactsByIds() validates that every requested company ID exists. It attaches all valid companies to each subscriber and sets the first company as primary only when company_id is empty.
Detach:
FluentCrmApi('companies')->detachContactsByIds([$contactId], [$companyId]);
Detach behavior:
- If a detached contact was the company owner,
owner_idis cleared. - If the detached company was the contact's primary
company_id, FluentCRM promotes the first remaining related company or setscompany_id = null.
For a single loaded contact, Subscriber::attachCompanies() and detachCompanies() are safe ORM-level helpers: they guard unsaved subscribers, cast IDs to ints, use per-row INSERT IGNORE / DELETE, refresh the relation, and only fire events for actual changes.
Hooks
Company record hooks:
fluent_crm/company_createdwith($company, $data)fluent_crm/company_updatedwith($company, $data)fluent_crm/before_company_deletewith($company)fluent_crm/company_deletedwith($companyId)fluent_crm/company_type_to_{type}with($company, $oldType)fluent_crm/company_category_to_{industry}with($company, $oldIndustry)
Contact-company pivot hooks are legacy underscore hooks only in 3.1.13:
fluentcrm_contact_added_to_companieswith($companyIds, $subscriber)fluentcrm_contact_removed_from_companieswith($companyIds, $subscriber)
Do not invent slash aliases for the pivot hooks; they are not emitted in the current source.
Query and segment contacts by company
For simple reads:
use FluentCrm\App\Models\Subscriber;
$contacts = Subscriber::with(['companies'])
->filterByCompanies([(int) $companyId])
->get();
ContactsQuery / advanced filters support company-aware properties:
- segment relation
companies company_industrycompany_type
Keep custom controllers bounded and allowlisted. Do not query serialized Company.meta for reporting unless you accept full-table scans.
Automation company actions
Core 3.1.13 registers ApplyCompanyAction and DetachCompanyAction only when Helper::isCompanyEnabled() is true. If a companion action depends on Companies, follow the same guard and seed getBlock()['settings'] with a company field default:
'settings' => [
'company' => null,
],
The built-in action field uses:
'type' => 'option_selectors',
'option_key' => 'companies',
That option key is provided by OptionsController::companies() and returns [{id, title}].
Common mistakes
- Using WP user IDs as
owner_id. It must be a FluentCRMSubscriberID. - Assuming
getCompany('Acme')searches by name. It searches by email for strings. - Setting only
Subscriber.company_idand skipping the pivot relation. The contact then has a primary company but is missing fromCompany::subscribers(). - Writing
Company.metamanually. Usecustom_valuesthrough the API soCustomCompanyFieldformats field values consistently. - Creating visible company integrations while
company_moduleis disabled.
Cross-references
- Use
fluentcrm-contact-modelsfor contact/list/tag basics. - Use
fluentcrm-funnel-actionwhen adding a custom company-aware automation action. - Use
fluentcrm-rest-optionswhen creating a custom company-like picker.
References
- Official documentation: https://developers.fluentcrm.com/database/orm/
- Verified source paths:
fluent-crm/app/Api/config.phpfluent-crm/app/Api/Classes/Companies.phpfluent-crm/app/Models/Company.phpfluent-crm/app/Models/CompanyNote.phpfluent-crm/app/Models/CustomCompanyField.phpfluent-crm/app/Models/Subscriber.phpfluent-crm/app/Services/Helper.phpfluent-crm/app/Http/Controllers/CompanyController.phpfluent-crm/app/Services/Funnel/Actions/ApplyCompanyAction.phpfluent-crm/app/Services/Funnel/Actions/DetachCompanyAction.phpfluent-crm/database/migrations/CompaniesMigrator.phpfluent-crm/database/migrations/Subscribers.php
Signals
- GitHub stars
- 22
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
fluentcrm-companies-model- Source
- github.com/lonsdale201/wp-agent-skills