The Subticket Manager plugin transforms osTicket's hidden parent/child ticket infrastructure into a fully functional, user-friendly feature. While osTicket provides the database structure (ticket_pid field) and API methods (isChild(), getPid()), it lacks any user interface or workflow automation.
This plugin provides a complete solution for managing hierarchical ticket relationships with visual indicators, workflow automation, and an intuitive interface for creating and managing subtickets.
What makes this plugin essential:
osTicket already has the technical foundation for parent/child tickets built into the core system, but it provides:
This plugin makes osTicket's hidden parent/child infrastructure actually usable for daily support operations.
ticket_pid), no core file changes required| Requirement | Version | Notes |
|---|---|---|
| osTicket | 1.18.x | Plugin uses osTicket's native ticket_pid infrastructure |
| PHP | 7.4+ | Recommended: PHP 8.1+ for best performance |
| jQuery | Any | Included in osTicket by default |
| Database | MariaDB 10.3+ or MySQL 5.7+ | Plugin creates additional metadata tables |
subticket-manager folder to /include/plugins/ on your osTicket serverFinal path: /path/to/osticket/include/plugins/subticket-manager/
cd /path/to/osticket/include/plugins
git clone https://github.com/markus-michalski/osticket-subticket-manager.git
scp/subtickets.php, scp/apps.php)scp/ajax-subticket.php)Check Admin Interface:
Check Ticket View:
Check Queue Indicators:
The plugin requires no configuration - it works out of the box using osTicket's native parent/child infrastructure.
All features are automatically enabled upon plugin activation:
Optional Settings:
SUBTICKET_DEBUG = true in class.SubticketPlugin.php (line 8) to enable detailed logging to /tmp/subticket-debug.logFrom Parent Ticket:
tickets.php?a=open&subticket_parent=15)model.created signal handler)Option 1: From Child Ticket
Option 2: From Parent Ticket
From Child Ticket:
ticket_pid set to NULL)From Parent Ticket:
Queue Lists:
queue-indicator.js) using AJAX endpointTicket View:
π Parent Ticket (3 Sub-Tickets)
background: #e8f4f8; border-left: 4px solid #1e90ff)Navigate to: Applications β Subticket Hierarchies (scp/subtickets.php)
Features:
The plugin provides standalone AJAX endpoints for frontend operations:
Base URL: /scp/ajax-subticket.php
POST /scp/ajax-subticket.php?action=link
Content-Type: application/x-www-form-urlencoded
child_id=123&parent_number=181752&__CSRFToken__=abc123
Response:
{
\"success\": true,
\"message\": \"Ticket linked successfully\"
}
POST /scp/ajax-subticket.php?action=unlink
Content-Type: application/x-www-form-urlencoded
child_id=123&__CSRFToken__=abc123
Response:
{
\"success\": true,
\"message\": \"Ticket unlinked successfully\"
}
GET /scp/ajax-subticket.php?action=batch_parent_status&ticket_ids=15,16,17
Response:
{
\"15\": {\"isParent\": true, \"childCount\": 3},
\"16\": {\"isParent\": false, \"childCount\": 0},
\"17\": {\"isParent\": true, \"childCount\": 1}
}
The plugin extends osTicket's QueueColumnAnnotation class to add parent indicators to queue SQL queries:
Class: ParentTicketDecoration (queue-decoration.php)
Method: annotate($query, $name = false)
Functionality:
LEFT JOIN to count child tickets(SELECT COUNT(*) FROM ost_ticket WHERE ticket_pid = ticket.ticket_id) as _subticket_countqueue-indicator.js to display icons in queue listsSymptoms:
Check:
// Open DevTools (F12) β Console tab
// Look for errors from queue-indicator.js
// In browser console:
typeof jQuery // Should return \"function\"
// In browser console:
SubticketPanel.debug = true
curl \"http://osticket.local/scp/ajax-subticket.php?action=batch_parent_status&ticket_ids=15\"
Symptoms:
ticket_pid is NOT NULLCheck:
Verify database relationship:
SELECT ticket_id, number, ticket_pid, status_id
FROM ost_ticket
WHERE ticket_id = 123;
Check if parent ticket exists:
SELECT * FROM ost_ticket WHERE ticket_id = (
SELECT ticket_pid FROM ost_ticket WHERE ticket_id = 123
);
Try unlinking and re-linking:
Check join query in plugin code:
/tmp/subticket-debug.log (if debug mode enabled)Symptoms:
Check:
Plugin is enabled:
Clear browser cache (may be hidden by old CSS)
Check if panel is collapsed:
<div class=\"subticket-panel section-break\"> in page source (F12 β Elements)Verify you're in staff interface:
Check signal registration:
define('SUBTICKET_DEBUG', true);/tmp/subticket-debug.log for:Signal registered: object.view for ticket view integration
onTicketView() called: Object: Ticket
Symptoms:
Check:
You have permission to create tickets:
Parent ticket is not closed:
tickets.php?a=open&subticket_parent=X)Check Apache error log:
tail -f /var/log/apache2/error.log
Session storage issue:
$_SESSION['subticket_parent'] is set after clicking button/tmp/subticket-debug.log:Stored subticket_parent in session: 15
Auto-linking ticket: Child: 456, Parent: 15
Symptoms:
Solution:
If issue persists:
include/plugins/ for duplicates)Symptoms:
Explanation:
The plugin prevents circular dependencies to maintain tree integrity. For example:
Solution:
Symptoms:
Check:
Check existing tables:
SHOW TABLES LIKE 'ost_ticket_hierarchy_metadata';
SHOW TABLES LIKE 'ost_ticket_progress';
Check existing columns:
SHOW COLUMNS FROM ost_ticket LIKE 'version';
Plugin already handles duplicates - If enable fails, check Apache error log for specific SQL error
Manual cleanup (if needed):
-- CAUTION: Only run if reinstalling!
DROP TABLE IF EXISTS ost_ticket_hierarchy_metadata;
DROP TABLE IF EXISTS ost_ticket_progress;
ALTER TABLE ost_ticket DROP COLUMN IF EXISTS version;
Plugin Structure:
subticket-manager/
βββ plugin.php # Plugin metadata
βββ class.SubticketPlugin.php # Main plugin class
βββ config.php # Plugin configuration class
βββ queue-decoration.php # Queue SQL enhancement (ParentTicketDecoration)
βββ ajax/
β βββ SubticketAjaxAPI.php # AJAX endpoint handlers (legacy)
βββ scp-files/
β βββ subtickets.php # Admin interface page (deployed to scp/)
β βββ apps.php # Applications overview page
β βββ ajax-subticket.php # Standalone AJAX handler
βββ js/
β βββ subticket-panel.js # Frontend panel interactions
β βββ queue-indicator.js # Queue list parent indicators
βββ services/
β βββ SubticketService.php # Business logic layer
β βββ SubticketCreator.php # Ticket creation service
βββ repositories/
β βββ SubticketRelationRepository.php # Database access layer
βββ validation/
β βββ SubticketValidator.php # Input validation
βββ cache/
β βββ SubticketCacheManager.php # Performance optimization
βββ events/
β βββ SubticketEventHandler.php # Signal handlers (auto-close, etc.)
βββ tests/
βββ Unit/ # PHPUnit tests
Signal Hooks:
| Signal | Handler | Purpose |
|---|---|---|
object.view |
onTicketView() |
Inject panel HTML into ticket view |
model.created |
onTicketCreated() |
Auto-link newly created subtickets |
model.updated |
onTicketStatusChanged() |
Trigger auto-close workflow (Phase 4) |
ajax.scp |
registerAjaxRoutes() |
Register AJAX endpoints (legacy, unused) |
Database Schema:
-- Additional tables created by plugin
CREATE TABLE ost_ticket_hierarchy_metadata (
ticket_id int(11) unsigned PRIMARY KEY,
auto_close_enabled tinyint(1) DEFAULT 1,
inherit_settings text,
dependency_type enum('blocks','depends_on','relates_to') DEFAULT 'relates_to',
max_children int(11) DEFAULT 50,
created timestamp DEFAULT CURRENT_TIMESTAMP,
updated timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES ost_ticket(ticket_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE ost_ticket_progress (
parent_id int(11) unsigned PRIMARY KEY,
total_children int(11) DEFAULT 0,
completed_children int(11) DEFAULT 0,
in_progress_children int(11) DEFAULT 0,
pending_children int(11) DEFAULT 0,
last_calculated timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES ost_ticket(ticket_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Indexes added to ost_ticket
CREATE INDEX idx_ticket_pid ON ost_ticket(ticket_pid);
CREATE INDEX idx_ticket_hierarchy ON ost_ticket(ticket_id, ticket_pid, status_id);
-- Column added to ost_ticket
ALTER TABLE ost_ticket ADD COLUMN version int(11) DEFAULT 0;
Input Validation:
db_input() used for SQL escaping (osTicket standard)Circular Dependency Prevention:
isDescendant() method checks ancestry up to 10 levelsSelf-Linking Prevention:
Permission Checks:
Single Query for Children:
getChildren() uses JOIN to fetch subject and status in one queryQueue Indicator Caching:
queue-indicator.js batches parent status checksAuto-Deploy Version Check:
.subticket-deployed-version fileDatabase Indexes:
idx_ticket_pid speeds up child lookupsidx_ticket_hierarchy optimizes parent-child queriesQ: Do I need to modify osTicket core files?
A: No! The plugin uses osTicket's native ticket_pid infrastructure and Signal system. All features work without touching core files.
Q: Will this work with my custom osTicket theme?
A: Yes, the plugin uses osTicket's standard CSS classes and structure. Visual indicators and panels adapt to your theme automatically.
Q: Can I upgrade osTicket without breaking the plugin?
A: Yes, as long as osTicket maintains the ticket_pid field and Signal system (which are core features). The plugin is tested against osTicket 1.18.x.
Q: How do I uninstall the plugin?
A: Admin Panel β Plugins β Subticket Manager β Delete. The plugin will:
scp/subtickets.php, scp/ajax-subticket.php, scp/apps.php)ticket_pid)Q: What's the maximum hierarchy depth?
A: The plugin supports up to 10 levels of nesting (configurable in isDescendant() method). This prevents infinite loops and maintains performance.
Q: Can a ticket have multiple parents?
A: No. osTicket's ticket_pid field only supports one parent per ticket. This matches the tree structure design.
Q: Can I bulk-link tickets?
A: Yes, use the Subticket Hierarchies admin page (Applications β Subticket Hierarchies) for batch operations.
Q: What happens if I delete a parent ticket?
A: By default, child tickets remain but their ticket_pid is set to NULL (unlinked). If you have foreign key constraints with ON DELETE CASCADE, children would also be deleted.
Q: Does auto-close work immediately?
A: Auto-close is a Phase 4 feature (currently in development). When all child tickets are closed, the parent will automatically close via model.updated signal handler.
Q: How are parent indicators added to queue lists?
A: The plugin uses two mechanisms:
ParentTicketDecoration class extends osTicket's QueueColumnAnnotation to add a SQL JOIN counting child ticketsqueue-indicator.js fetches parent status via AJAX and injects icons dynamicallyQ: Why use standalone AJAX handler instead of Signal routing?
A: The standalone handler (ajax-subticket.php) is simpler and more maintainable than Signal-based routing. It provides direct, predictable endpoints without complex dispatcher logic.
Q: How does auto-linking work for new subtickets?
A: When you click "Create Subticket", the parent ID is stored in $_SESSION['subticket_parent']. After the ticket is created, the model.created signal handler (onTicketCreated()) automatically sets ticket_pid in the database.
Q: Can I customize the panel appearance?
A: Yes, modify getPanelCSS() method in class.SubticketPlugin.php. All styles are inline CSS for easy customization.
Q: How do I enable debug logging?
A: Edit class.SubticketPlugin.php and change line 8:
define('SUBTICKET_DEBUG', true);
Debug logs are written to /tmp/subticket-debug.log.
Q: What SQL queries are executed?
A: Enable debug mode and check /tmp/subticket-debug.log. Example queries:
-- Get children
SELECT t.ticket_id, t.number, cdata.subject, s.name as status
FROM ost_ticket t
LEFT JOIN ost_ticket__cdata cdata ON t.ticket_id = cdata.ticket_id
LEFT JOIN ost_ticket_status s ON t.status_id = s.id
WHERE t.ticket_pid = 15;
-- Link ticket
UPDATE ost_ticket SET ticket_pid = 15 WHERE ticket_id = 123;
Q: Does this work with osTicket 1.17 or older?
A: No. The plugin requires osTicket 1.18.x for proper Signal support and database structure. osTicket 1.17 and older may have different ticket_pid implementations.
Q: Is it compatible with PHP 8.x?
A: Yes! The plugin is tested with PHP 7.4, 8.0, 8.1, 8.2, and 8.3. CI pipeline runs tests on all versions.
Q: Can I use this with PostgreSQL instead of MySQL?
A: The plugin is designed for MySQL/MariaDB. PostgreSQL support would require modifying SQL queries (osTicket itself primarily supports MySQL).
Q: Does it work with Elasticsearch queue plugin?
A: Should work, but not explicitly tested. The queue decoration hooks into osTicket's standard queue system, so Elasticsearch plugins may override it.
This Plugin is released under the GNU General Public License v2, compatible with osTicket core.
See LICENSE for details.
For questions or issues, please create an issue on GitHub:
Issue Tracker: https://github.com/markus-michalski/osticket-subticket-manager/issues
When reporting issues, please include:
php -v)Developed by Markus Michalski
Contributions welcome!
See CHANGELOG.md for version history.