perfex-customfields
Perfex Custom Fields
You are a Perfex CRM custom-fields specialist. Your job is to read, write, and install custom fields against tblcustomfields and tblcustomfieldsvalues without tripping over Perfex's quirks — the misspelled disalow_client_to_edit column, only_admin visibility, bs_column Bootstrap sizing, and module-prefixed slug conventions.
Custom fields are Perfex's extensibility mechanism for adding user-defined fields to contacts, clients, leads, invoices, tickets, and most core entities. Two tables: tblcustomfields (definitions) and tblcustomfieldsvalues (values keyed by relid).
Schema gotchas (critical)
only_admin — NOT only_admin_area
The column is only_admin. Some older docs and Stack Overflow answers refer to only_admin_area — that's wrong. Don't alias, don't "fix".
disalow_client_to_edit — the typo is canonical
Yes, it's misspelled (missing 'l' after 'disa'). Preserve it. Perfex core queries this exact column name. If you rename it, core breaks. If you write an abstraction over it, leave the DB column alone and only alias in PHP.
Full definition-row shape
When inserting a custom field programmatically:
$CI->db->insert(db_prefix() . 'customfields', [
'fieldto' => 'contacts', // 'customers', 'contacts', 'leads', 'invoice', 'estimate', 'contracts', 'tasks', 'expenses', 'tickets', 'proposal', 'subscriptions', 'items'
'name' => 'Study Program',
'slug' => 'contacts_study_program', // must be unique per fieldto
'required' => 0,
'type' => 'input', // input | textarea | select | multiselect | checkbox | number | date | date_picker | datetime | link | colorpicker | file
'options' => '', // newline-separated for select types; null for input
'display_inline' => 0,
'field_order' => 0,
'active' => 1,
'disalow_client_to_edit'=> 0, // ← preserve the typo
'only_admin' => 0, // 1 = hide from client area
'show_on_pdf' => 0,
'show_on_client_portal' => 1,
'show_on_table' => 0,
'show_on_picker' => 0, // on invoice/estimate item picker
'default_value' => '',
'bs_column' => '12', // '12' | '6' | '4' | '3' — Bootstrap grid width
'has_permission_view' => 0, // 0 = all staff can view
'permission_view' => '', // comma-separated staff IDs if has_permission_view=1
]);
Module-owned custom fields — convention
Prefix your module's slugs with the module name and the entity:
onboarding_passport_number → contacts.onboarding_passport_number
mymodule_plan_type → customers.mymodule_plan_type
This prevents slug collisions with core and other modules.
Reading values
Values live in tblcustomfieldsvalues with (fieldid, relid) as the natural key. relid is the ID of the parent record (contact ID, invoice ID, etc.).
public function get_custom_field_value($fieldto, $relid, $slug) {
$this->db->select('v.value');
$this->db->from(db_prefix() . 'customfieldsvalues v');
$this->db->join(db_prefix() . 'customfields f', 'f.id = v.fieldid');
$this->db->where('v.relid', $relid);
$this->db->where('f.fieldto', $fieldto);
$this->db->where('f.slug', $slug);
$row = $this->db->get()->row();
return $row ? $row->value : null;
}
Perfex also provides get_custom_field_value($relid, $field_id, $field_to) as a helper — but you need the field ID. Looking up by slug is usually more maintainable.
Writing values
There's no simple "set" helper. Pattern: upsert via delete+insert, or check-then-update:
public function set_custom_field_value($fieldto, $relid, $slug, $value) {
$field = $this->db->select('id')
->where(['fieldto' => $fieldto, 'slug' => $slug])
->get(db_prefix() . 'customfields')->row();
if (!$field) return false;
$existing = $this->db->where(['fieldid' => $field->id, 'relid' => $relid])
->get(db_prefix() . 'customfieldsvalues')->row();
if ($existing) {
$this->db->where(['fieldid' => $field->id, 'relid' => $relid]);
$this->db->update(db_prefix() . 'customfieldsvalues', ['value' => $value]);
} else {
$this->db->insert(db_prefix() . 'customfieldsvalues', [
'fieldid' => $field->id,
'relid' => $relid,
'value' => $value,
]);
}
return true;
}
Field types — what the type column means
| type | options format |
Storage in value column |
|---|---|---|
input |
null | plain string |
textarea |
null | plain string |
select |
Opt1\nOpt2\n… |
the selected string |
multiselect |
Opt1\nOpt2\n… |
comma-separated strings |
checkbox |
Opt1\nOpt2\n… |
comma-separated strings |
number |
null | numeric string |
date |
null | YYYY-MM-DD |
date_picker |
null | YYYY-MM-DD |
datetime |
null | YYYY-MM-DD HH:MM:SS |
link |
null | URL string |
colorpicker |
null | hex string #rrggbb |
file |
null | filename relative to uploads/ |
options is plain newlines — not JSON, not comma-separated. Perfex explodes by \n at render time.
Bootstrap column width (bs_column)
Controls visual width in the admin/client form. Allowed values: '12', '6', '4', '3'. Stored as a string. Sets the Bootstrap 3 grid class col-md-N.
Programmatically installing fields in a module
In your install.php:
$fields = [
[
'fieldto' => 'contacts',
'slug' => 'mymodule_plan',
'name' => 'Plan',
'type' => 'select',
'options' => "Basic\nPro\nEnterprise",
'bs_column' => '6',
],
// ...
];
foreach ($fields as $f) {
$exists = $CI->db->where(['fieldto' => $f['fieldto'], 'slug' => $f['slug']])
->get(db_prefix() . 'customfields')->num_rows();
if ($exists) continue;
$CI->db->insert(db_prefix() . 'customfields', array_merge([
'required' => 0, 'active' => 1, 'only_admin' => 0,
'disalow_client_to_edit' => 0, 'show_on_client_portal' => 1,
'display_inline' => 0, 'field_order' => 0, 'show_on_pdf' => 0,
'show_on_table' => 0, 'show_on_picker' => 0,
'default_value' => '', 'has_permission_view' => 0, 'permission_view' => '',
'bs_column' => '12', 'options' => '',
], $f));
}
Rendering in a custom view
Perfex ships render_custom_fields():
<?= render_custom_fields('contacts', $contact_id, [
'print_only_required' => false,
'only_customer_portal' => false,
]); ?>
In client-area views, pass 'only_customer_portal' => true to respect only_admin and show_on_client_portal.
Don't assume core columns haven't drifted
Older Perfex installs (pre-2.9) may lack show_on_client_portal and bs_column. Before writing migration or install code that references them, run:
SHOW COLUMNS FROM `tblcustomfields`;
If your code will run on older installs, wrap inserts with defensive column inclusion (only set a column if it exists).
Related skills
perfex-database—tblcustomfieldsschema (only_admin,disalow_client_to_edit) and why you can't "fix" the typo.perfex-module-dev— programmatically installing fields in a module'sinstall.php.perfex-core-apis—_l()for localized field labels when rendering.
Upstream docs
- Perfex custom fields: https://help.perfexcrm.com/custom-fields/
More from yasserstudio/perfex-crm-skills
perfex-email
Use whenever the user is sending, rendering, or debugging transactional email in a Perfex CRM module — `$this->emails_model->send_simple_email`, `send_mail_template`, email template files under `views/emails/`, admin-recipient fallback chains (`my_module_admin_email` → `contact_form_notification_email` → `smtp_email`), retry queues with exponential backoff stored in `tbl<module>_email_retries`, or cron-driven retry processing via `after_cron_run`. Also trigger when the user says "my Perfex email isn't sending", "send_simple_email returns false", "email failed but the user saw a success page", "SMTP error in my module", "email retry queue", "why didn't my notification email go out", or "email template merge fields". Reinforces the rule that email failure must never break the user flow — always try/catch and enqueue on failure.
1perfex-security
Use whenever a Perfex CRM task touches security-sensitive code — issuing or consuming single-use tokens (password reset, magic link, confirmation), race-safe atomic UPDATE with `affected_rows()` check, handling user-controlled redirect URLs (`?next=`, `?redirect=`, `?return_to=`), rate-limiting an AJAX endpoint that leaks boolean state, cross-module model loads, logging PII, adding `target="_blank"` links, or excluding a webhook from CSRF. Also trigger when the user says "my magic link works twice", "password reset is racy", "someone can enumerate users by email", "open redirect in my module", "CSRF blocking my webhook", "rate limit this endpoint", or mentions "TOCTOU", "enumeration", `html_purify`, or `app_generate_hash()`. Every rule here exists because its absence caused a real Perfex production incident.
1perfex-core-apis
Use whenever the user is working inside a Perfex CRM codebase and touches `get_option`, `update_option`, `add_option`, `delete_option`, `hooks()`, `do_action`, `apply_filters`, `register_activation_hook`, `$this->load`, `get_instance()`, `$CI`, `db_prefix()`, auth helpers like `is_staff_logged_in` / `get_staff_user_id` / `staff_can`, or `_l()`. Also trigger when the user says "my Perfex get_option returns empty", "the hook isn't firing", "how do I hook into Perfex", "module-wide option", "Perfex helper function", "CI loader inside Perfex", or "$CI doesn't work outside a controller". This skill prevents the #1 Perfex bug — silently using `get_option('key', 'default')` which ignores the second argument.
1perfex-theme
Use whenever the user is building or debugging a Perfex CRM custom client-area theme — files under `assets/themes/<theme>/` and `application/views/themes/<theme>/`, asset injection via `app_customers_head`/`app_customers_footer`/`app_admin_head`/`app_admin_footer` hooks, overriding core views, dark mode with `[data-theme="dark"]` plus anti-FOUC `<head>` scripts, RTL/Arabic support, or the jQuery Validate bug where a submit button's `name` is stripped from POST (breaks "Pay Now" / "Save Draft" detection). Also trigger when the user says "my theme CSS is cached after deploy", "Pay Now button loses its value", "jQuery Validate ate my button name", "client area dark mode", "theme file isn't picked up on Linux", or "FOUC when switching themes".
1perfex-database
Use whenever the user writes SQL DDL for a Perfex CRM module, adds a foreign key referencing `tblcontacts`, `tblstaff`, `tblclients`, `tblinvoices`, or any `tbl*` core table, designs `tbl<module>_<entity>` schema, writes `install.php` / `uninstall.php` DDL, writes a migration or `ALTER TABLE`, or debugs "Cannot add foreign key constraint" / "incompatible" errors. Also trigger when the user says "FK won't create in Perfex", "my module's table has wrong collation", "schema in staging differs from prod", "add a column to my Perfex module table", or mentions `db_prefix()` in a DDL context, `utf8mb4_unicode_ci`, or `VARCHAR(191)` vs `VARCHAR(255)`. Prevents the UNSIGNED-INT-vs-signed-INT trap that silently drops foreign-key constraints pointing at Perfex core tables.
1perfex-module-dev
Use whenever the user is creating, modifying, or debugging a Perfex CRM module — anything under `modules/<module_name>/` including `module_name.php`, `install.php`, `uninstall.php`, controllers extending `AdminController` or `ClientsController`, models extending `App_Model`, views, language files, or menu items via `app_menu->add_sidebar_menu_item`. Also trigger when the user says "my Perfex module won't install", "activation hook not running", "the module doesn't show up in Setup", "controller returns 404", "model not loading in Perfex", "admin menu item not showing", or "build a new Perfex module from scratch". Covers module lifecycle, CI3 controller conventions, and the Linux case-sensitivity trap that silently breaks model loading on production.
1