Repository navigation
Custom Ticket Fields Developer Guide
How custom ticket fields (discussion #179) work underneath. For what they do, see Custom ticket fields.
| File | What it does |
|---|---|
includes/services/ticket_fields.php |
TicketFieldsService - which fields a ticket has, validation, writing with history, the closing check, and designing fields |
includes/typed_fields.php |
TypedFields - the shared engine: types, coercion, required rule, value storage. Shared with the CMDB and custom asset fields |
includes/services/tickets.php |
createTicket() / updateTicket() take custom_fields
|
includes/services/mandatory_fields.php |
missing() adds custom fields required at closing |
includes/self_service_ticket_types.php |
The portal's types and company, shared by its form and its create |
assets/js/ticket-fields.js + assets/css/ticket-fields.css
|
One renderer for the New ticket form, the ticket panel and the portal |
api/tickets/get_ticket_custom_fields.php / save_ticket_custom_fields.php
|
The inbox's read and per-field save |
api/tickets/get_ticket_fields_settings.php, save_ticket_field.php, delete_ticket_field.php, reorder_ticket_fields.php
|
The settings tab |
api/self-service/get_ticket_fields.php |
The portal form: types (when they matter) and asked fields |
api/self-service/create_ticket.php |
The portal create: type and answers re-checked |
api/v1/resources/tickets.php |
custom_fields on GET / POST / PATCH |
tests/ticket-custom-fields.php |
The test (section 9) |
| Table | Holds |
|---|---|
ticket_fields |
the definitions: field_key (immutable), label, field_type, config JSON, help_text, all_types, required_on_create, required_on_close, ask_requester, display_order, tenant_id, is_deleted
|
ticket_field_options |
dropdown choices; the answer stores the option's text |
ticket_field_types |
which types a field is on when all_types = 0
|
ticket_field_values |
the answers, one typed column each - the same shape as asset_field_values
|
-
Soft delete only.
ticket_field_values.field_idis aRESTRICTforeign key indatabase/freeitsm.sql, so a field with answers can only be retired. -
seqis always 0, reserved so a multi-value field needs no index change later (Database Verification restores columns, not a changed unique index). -
uq_ticket_fields_tenant_keydoes not dedupe shared keys (MySQL treats NULLs as distinct), sofreeKey()checks in code - the trapasset_fieldsdocuments. -
No
tenant_idon values - inherited from the ticket. -
ref(link fields) is not offered on tickets: it needs a picker.TicketFieldsService::TYPESis the list.
Resolved in one place, TicketFieldsService::fieldsFor($tenantId, $typeId, $audience):
not retired and (shared or the ticket's company's own) and (on every type or linked to the ticket's type) and, for
'requester',ask_requester = 1.
Everything else asks it: the New ticket form, the panel, the portal, the API, the closing rule. Two copies of this rule would be the bug.
π Type changes hide, never delete. TypedFields::readValues() only returns values for the definitions it is handed, so an answer to a field the new type lacks is simply not read. Nothing removes it.
toDef() sets the engine's required to required_on_create, so TypedFields::checkRequired() enforces it with the CMDB's asymmetry: missing altogether is refused on create; on update only fields being written are checked - so a required field cannot be cleared, but editing another field on an older ticket never fails because of it.
Logging - only the channels that can ask:
| Channel |
custom_fields passed? |
Enforced? |
|---|---|---|
New ticket form (api/tickets/create_ticket.php) |
yes, always | β |
REST API POST /tickets
|
yes - defaulted to []
|
β |
Portal (api/self-service/create_ticket.php) |
yes, requester fields only | β for asked fields |
Morning checks' raise-a-ticket (same endpoint, no custom_fields) |
no | β |
| Email, chat, workflows, project and domain alerts | no | β |
π createTicket() enforces only when the custom_fields key is present. Present means "this channel can ask". That is the switch - an alert raised by the system must never be refused because nobody filled in a form it never showed.
Closing - MandatoryFieldsService::missing($conn, $tenant, $row, $pendingCustom) appends TicketFieldsService::missingOnClose() as cf:<key> => label, even when no built-in field is ticked, so the tab's mode (warn / notify / block) and its note and email cover custom fields too. $pendingCustom is the answers being written by the same updateTicket(), so filling a field in the save that closes the ticket counts. The inbox's pre-close check (get_mandatory_fields_check.php) gets the label through t()'s fallback to English - the name an admin typed.
-
Validate first, write later.
validate()runs unknown-key, required, email / URL format, length and everyTypedFields::coerce()rule before anything is written. IncreateTicket()and the portal create it runs before theINSERT, so a bad answer never leaves a half-made ticket. -
updateTicket()resolves the definitions againstrowAfterUpdates()- a type changed in the same request decides which fields apply - and writes after theUPDATE. A save with only custom fields still touchesupdated_datetime. -
History is always server-side.
write()adds oneticket_auditrow per changed field,field_name= the label (cut to 100), values as a person reads them (displayValue(), cut to 500), whatever$writeAuditsays - the UI never logs these client-side. A portal answer is audited with a NULL analyst.
assets/js/ticket-fields.js exposes TicketFields.render(fields, opts), collect(container) and firstMissing(container, fields).
-
variant: 'toolbar'- the panel. Rendered into#ticketCustomFields(display: contents) inside.ticket-toolbar, so each field is a real grid item beside the built-in ones and the phone layout's restyling of the toolbar applies unchanged. -
variant: 'form'- the New ticket forms, with the form's own classes (inputClass/selectClass/labelClass) so a custom field is indistinguishable from the built-in ones. -
collect()sends an empty control as''- clear - never omits it. - Yes / no has three states, Not set included, exactly as the asset renderer.
- The panel reloads its fields after a type change (
assignTicketType()); the New ticket form after a type or company change, keeping answers already typed.
selfServiceTicketTypeMatters() - the picker appears only when an asked field is on particular types; otherwise choosing a type would change nothing. Types offered and types accepted come from one function, selfServiceTicketTypes(), so a hand-posted type the company does not offer is dropped, and a hand-posted key for an analyst-only field is a refusal (fieldsFor(..., 'requester') does not contain it). A chosen type also numbers the ticket per type, as other channels do.
designScope() copies ticket types: working in the default company (or on a one-company install) manages shared fields; working in a client company manages that company's own, and sees shared ones read-only. loadEditable() enforces it on every save, retire and reorder, and a field may only be linked to types that context can see (assertTypeUsable()). The capability is Cap::TICKETS_CUSTOM_FIELDS.
php tests/ticket-custom-fields.php - 29 checks on the real tables with throwaway ZZTF types, fields and one ticket inserted directly (no workflow, email or notification fires), removed at the end:
- which fields a type has, and what the requester is asked;
- required-when-logging refusals, including
createTicket()refusing before any row is written; - dropdown, email and unknown-key refusals;
- answers stored, shown, audited; a required one cannot be cleared;
- a type change hides an answer, the row stays, changing back restores it;
- missing at closing, and not missing when the same save fills it in;
- the kind lock, a second field with the same name getting
_2, retiring keeping answers; - the portal's type picker rule.
Checked in a browser as well: the settings tab and dialog, the New ticket form, the panel, and the portal on desktop and phone - and the portal create refusing a missing answer, an analyst-only key and a bad option without creating a ticket.
- Workflow conditions (
engine.phpfield lists), email merge codes (buildTicketMergeData()), report packs and dashboard widgets, search - each is a hard-coded list that would have to learncf:<key>. - Split and merge do not copy answers to the new ticket.
- The portal ticket page does not show the requester their answers.
- Translations: field names and options are data, like ticket type names, and are shown as typed.
- Custom asset fields - Developer Guide - the same engine, with sets
- Mandatory fields - Developer Guide
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96