$npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-contact-modelsWork with FluentCRM 3.x contact data through the public PHP API and ORM models. Covers Subscriber, Lists, Tag, User, ContactsQuery, createOrUpdate, list/tag attach and detach, custom fields, WP user linking, status protection, and contact hooks. Use when a plugin must create or u
| 1 | # FluentCRM: contact, list, tag, and user models |
| 2 | |
| 3 | Use this skill for companion plugins that need to write or query FluentCRM contacts. Prefer `FluentCrmApi()` wrappers for writes, and use the ORM models for reads, reports, migrations, and carefully scoped queries. |
| 4 | |
| 5 | Verification note: this skill is based on FluentCRM core 3.1.8 source. The contact/list/tag/user APIs covered here are core APIs and do not require FluentCampaign Pro. |
| 6 | |
| 7 | ## When to use this skill |
| 8 | |
| 9 | - Creating or updating contacts from a third-party plugin, webhook, form, order, LMS event, or user registration. |
| 10 | - Adding or removing FluentCRM lists/tags from a contact. |
| 11 | - Querying contacts by list, tag, status, SMS status, company, search, custom field, or advanced filter provider. |
| 12 | - Mapping a WordPress user to a FluentCRM contact. |
| 13 | - Reviewing code that touches `Subscriber::create()`, `Subscriber::updateOrCreate()`, `attachLists()`, `attachTags()`, `fluentcrm_subscriber_statuses()`, or `ContactsQuery`. |
| 14 | |
| 15 | ## API entry points |
| 16 | |
| 17 | Guard companion plugin code and run after FluentCRM has loaded: |
| 18 | |
| 19 | ```php |
| 20 | if (!function_exists('FluentCrmApi')) { |
| 21 | return; |
| 22 | } |
| 23 | |
| 24 | $contactApi = FluentCrmApi('contacts'); |
| 25 | $listApi = FluentCrmApi('lists'); |
| 26 | $tagApi = FluentCrmApi('tags'); |
| 27 | ``` |
| 28 | |
| 29 | `app/Api/config.php` registers these keys: `contacts`, `tags`, `lists`, `extender`, `companies`, and `event_tracker`. |
| 30 | |
| 31 | Prefer: |
| 32 | |
| 33 | ```php |
| 34 | $contact = FluentCrmApi('contacts')->createOrUpdate([ |
| 35 | 'email' => sanitize_email($email), |
| 36 | 'first_name' => sanitize_text_field($firstName), |
| 37 | 'last_name' => sanitize_text_field($lastName), |
| 38 | 'user_id' => (int) $userId, |
| 39 | 'status' => 'subscribed', |
| 40 | 'source' => 'my-plugin', |
| 41 | 'lists' => [3], |
| 42 | 'tags' => [12], |
| 43 | 'custom_values' => [ |
| 44 | 'plan' => sanitize_text_field($plan), |
| 45 | ], |
| 46 | ], false, false); |
| 47 | ``` |
| 48 | |
| 49 | Do not create contacts with raw `$wpdb` inserts. Direct `Subscriber::create()` skips several integration-level behaviors. `FluentCrmApi('contacts')->createOrUpdate()` delegates to `Subscriber::updateOrCreate()`, syncs lists/tags/custom fields, links a WP user by email when possible, and fires the contact lifecycle hooks. |
| 50 | |
| 51 | ## Contact lookup |
| 52 | |
| 53 | Use the API wrapper for common lookup: |
| 54 | |
| 55 | ```php |
| 56 | $contact = FluentCrmApi('contacts')->getContact($idOrEmail); |
| 57 | $contact = FluentCrmApi('contacts')->getContactByUserRef($userIdOrEmail); |
| 58 | $contact = FluentCrmApi('contacts')->getCurrentContact(); |
| 59 | ``` |
| 60 | |
| 61 | `getContactByUserRef($userId)` first checks `user_id`, then falls back to the WP user's email and saves the `user_id` on the contact if found. `Subscriber::getWpUser()` performs the inverse lookup and also removes duplicate `user_id` links from other contacts. |
| 62 | |
| 63 | ## Status rules |
| 64 | |
| 65 | Use `fluentcrm_subscriber_statuses()` for the current status list. In FluentCRM 3.1.8 the local source returns: |
| 66 | |
| 67 | ```php |
| 68 | [ |
| 69 | 'subscribed', |
| 70 | 'pending', |
| 71 | 'unsubscribed', |
| 72 | 'transactional', |
| 73 | 'bounced', |
| 74 | 'complained', |
| 75 | 'spammed', |
| 76 | ] |
| 77 | ``` |
| 78 | |
| 79 | `fluentcrm_subscriber_editable_statuses()` excludes `bounced`, `complained`, and `spammed`. `fluentcrm_strict_statues()` returns `unsubscribed`, `bounced`, `complained`, and `spammed`. |
| 80 | |
| 81 | Important write behavior: |
| 82 | |
| 83 | - Without `$forceUpdate`, an existing `subscribed` contact is not downgraded by incoming `status`. |
| 84 | - Existing `bounced`, `complained`, and `spammed` contacts keep their status unless forced. |
| 85 | - Incoming `unsubscribed` is always respected. |
| 86 | - Use `$contact->updateStatus($status)` for an explicit status change; it fires `fluent_crm/subscriber_status_changed` and the legacy `fluentcrm_subscriber_status_to_{status}` hook. |
| 87 | |
| 88 | ## Custom fields |
| 89 | |
| 90 | Pass custom fields under `custom_values`: |
| 91 | |
| 92 | ```php |
| 93 | FluentCrmApi('contacts')->createOrUpdate([ |
| 94 | 'email' => $email, |
| 95 | 'custom_values' => [ |
| 96 | 'customer_tier' => 'gold', |
| 97 | 'renewal_date' => '2026-12-31', |
| 98 | ], |
| 99 | ], false, false); |
| 100 | ``` |
| 101 | |
| 102 | The third `createOrUpdate()` argument maps to `syncCustomFieldValues($values, $deleteOtherValues)`. Keep it `false` for incremental update |