-
Notifications
You must be signed in to change notification settings - Fork 15
Demo Data Developer Guide
How the demo-data importer works, and how to add demo data for a new module. The user-facing page is Demo Data.
The design goal: a module's sample data is a single JSON file of plain rows, and one generic importer turns it into a correct, cross-referenced, repeatable database seed. Adding demo data should mean writing JSON β not code.
Colour key: π data Β· βοΈ importer Β· π₯οΈ UI Β· π diagnostic Β· π docs
| File | What it does | |
|---|---|---|
| π | database/demo-data/<module>.json |
the sample rows for one module (e.g. core.json, tickets.json) |
| βοΈ | api/system/import_demo_data.php |
the engine. Loads the JSON, resolves references + tokens, inserts in a transaction |
| π₯οΈ | system/demo-data/index.php |
the System β Demo Data page β one card/button per module |
| π | api/system/debug-tools/D001_demo_core_import.php |
the Demo Core Data Import diagnostic (traces a failing core import) |
| π | this wiki | you are here |
There is no per-module import code β the importer is generic. Everything specific to a module lives in its JSON.
A demo file is an object of tiers, each tier an object of tables, each table a list of rows:
{
"tier1": {
"departments": [
{ "_ref": "it", "name": "IT Support", "is_active": 1, "display_order": 1 }
]
},
"tier2": {
"teams": [
{ "_ref": "servicedesk", "name": "Service Desk", "is_active": 1 }
]
},
"tier3": {
"analyst_teams": [
{ "analyst_id": "@analysts.jsmith", "team_id": "@teams.servicedesk" }
]
}
}-
The table key is the real database table name. Row fields are its columns. The importer builds the
INSERTfrom whatever keys are present β so a row is just "the columns I want to set". -
Tiers are ordering buckets, processed
tier1 β tier5. Their only job is dependency order: a row that references another must live in a later tier (or a later position in the same tier) than the row it points at. Name them for meaning if you like, but the importer only cares about order.
These are stripped before insert β they control the importer, they are not columns.
| Field | Purpose |
|---|---|
_ref |
Name this row so others can reference it. Becomes @<table>.<ref> (see Β§4). |
_skip_insert + _match_by + _match_value
|
Don't insert β match an existing row and map its id. Used for the admin account: { "_ref": "admin", "_skip_insert": true, "_match_by": "username", "_match_value": "admin" }. The row is looked up, its id recorded under @<table>.<ref>, and it is preserved when the table is cleared. |
Any string value of the form @<table>.<ref> is replaced with the real primary-key id of the row that declared that _ref, at insert time:
{ "analyst_id": "@analysts.jsmith", "team_id": "@teams.servicedesk" }Because ids are assigned as rows insert, the referenced row must already have been inserted β hence tiers. An unresolved reference throws and rolls back the whole import, so a typo fails loudly rather than writing a bad row.
π Referencing rows in the same table you're currently inserting works too, as long as the target appears earlier in the list.
Any string value matching one of these is expanded per row:
| Token | Expands to |
|---|---|
__BCRYPT:secret__ |
a bcrypt hash of secret (demo passwords β the analysts use __BCRYPT:demo1234__) |
__RELATIVE_DATE:-60d__ |
a UTC datetime N days from now; optional hours: __RELATIVE_DATE:-60d+3h__
|
__RELATIVE_DATEONLY:-7d__ |
a UTC date (no time) N days from now |
__NOW__ |
the current UTC datetime |
__GENERATE__ |
a fresh demo ticket number (tickets only) |
__UNIQUE__ |
replaced with a uniqid() (embed it in a larger string) |
Relative dates are why a freshly imported ticket looks recent: stamp demo dates relatively, never with a literal date.
Some tables store a status/priority/etc. as an id into a lookup table, but the demo JSON is written with the human-readable name for legibility. The importer swaps them using a small table in import_demo_data.php:
$lookupTranslations = [
'tickets' => [
['status', 'status_id', 'ticket_statuses'],
['priority', 'priority_id', 'ticket_priorities'],
],
// changes, tasks, status_incidents, ...
];So "status": "Open" in tickets.json becomes status_id = <id of 'Open'>. An unknown name throws. If your new module normalises a field into a lookup table, add a row here β otherwise write the real *_id (or an @ref) in the JSON.
A couple of bespoke cases live near it: morningChecks_Results resolves Status via Label (not name), and tickets drops legacy requester_email / requester_name. Follow that pattern only if a table genuinely needs it.
-
One transaction. Any error β full rollback, nothing changes. The response is
{ success, imported: {table: count}, total }. -
Repeatable. Before inserting, every table that has insertable rows is cleared (
DELETE, FK checks off, reverse tier order)._skip_insertrows are preserved via aWHERE NOT (...)guard β that's how the admin account survives. -
Primary keys. Ids are mapped via
getPrimaryKeyColumn(), which defaults toid. Override it there for a table whose PK isn'tid(e.g.morningChecks_ChecksβCheckID), or map tonullfor a pure join table with no id worth referencing.
-
Write
database/demo-data/<module>.jsonβ tiers, tables, rows, using_ref/@refs/ tokens above. Order tables so referenced rows insert first. -
Allowlist it β add
<module>to$allowedModulesinapi/system/import_demo_data.php(the import refuses any module not on this list). -
Add a card for it in
system/demo-data/index.phpso it appears on the Demo Data page, with a one-line description of what it seeds. -
Lookup translations (only if needed) β if the module stores a name-as-id, add its columns to
$lookupTranslations(Β§6). -
Primary key (only if needed) β if a table's PK isn't
id, add it togetPrimaryKeyColumn()(Β§7). - Test on a fresh install (a throwaway Docker instance is ideal): import Core Data first, then your module. Confirm the counts, and that re-importing gives a clean copy, not duplicates.
π Core Data first, always. Nearly every module references analysts, teams, departments or users by
@ref, and those refs are only in scope oncecore.jsonhas been imported in the same database. Document any cross-module dependency in the module's Demo Data card.
If a core import misbehaves, the D001 β Demo Core Data Import debug tool (System β Debug tools) runs the whole thing in-process and reports table-by-table what happened, including a write-probe inside a rolled-back transaction.
core.json seeds a realistic role-based-access example purely as data β no importer changes were needed, which is the whole point of the generic design. It:
- sets demo analysts non-admin with
can_access_all_modules = 0; - gives the Escalation Team
can_access_all_modules = 1and an Escalation Teamrbac_roleholding every capability (rbac_role_capabilities, one row per key fromcapAll()); - scopes the Service Desk team to a 12-module subset via
team_modules, with a blank Service Desk role; - links roles to teams via
rbac_team_roles.
All of it is @ref wiring across rbac_roles, rbac_role_capabilities, rbac_team_roles, team_modules, teams and analysts β a good template for any permissions-related demo. See the Demo Data user page for what it looks like in the UI.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)