Docs / Admin

Issue tracking and configurable issue types

Summary — The issues module is a generic, configurable tracker. Admins define issue types (slug, display name, status set, and custom fields) at /admin/issues/types. Staff then create and work issues of those types at /issues, updating status and adding comments.

Purpose — Understand how to configure an issue type's custom fields and allowed statuses, how staff create and manage issues day to day, and what happens when a status transition is attempted that the issue type does not allow.

Audience — Admins who configure issue types. Staff, reviewer, lead, and admin roles who create and work issues.

Prerequisites — None. Issue types must exist before staff can create issues (the create form has no fallback if none are configured).


Overview

An issue type defines the shape of a category of trackable work: its slug, display name, icon, default status, allowed statuses, and any custom fields. An issue is one record of that type — a title, description, priority, assignment, due date, tags, and the type's custom fieldData.

Key concepts:

  • Issue type — Configuration record (issue_types table) with slug, displayName, defaultStatus, allowedStatuses (array), fields (array of custom field definitions), and isActive.
  • Issue — A work item (issues table) referencing an issueTypeId, with status, priority, assignedTo, dueDate, tags, and fieldData (a JSON object holding values for the type's custom fields).
  • Custom fields — Per-type field definitions (slug, label, type, required, options) stored on the issue type and rendered by staff when filling in fieldData.
  • Status transitions — Changing an issue's status is validated against the owning issue type's allowedStatuses array; an illegal value is rejected.
  • Comments — Free-text notes on an issue (issue_comments table), with a commentType of comment, status_change, assignment, or system. Status changes automatically create a status_change comment.
  • Linking — Issues carry optional linkedEntityType / linkedEntityId fields so an issue can reference another record elsewhere in the platform, and can be filtered by those fields via listIssuesAction.

Screenshot: Issue Types admin table showing Slug, Display Name, Icon, Fields count, Active badge, and Edit link columns.


Configuring an issue type

Path: /admin/issues/typesCreate Type button

This opens /admin/issues/types/new, a three-card form:

Basic Information

FieldNotes
Display Name*Required. E.g., "Bug Report". Typing here auto-fills the slug (via slugify()) unless the slug field has been manually edited.
Slug (auto-generated)Editable text field, kept in sync with Display Name until touched directly.
DescriptionOptional free text.
IconSelected via the IconPicker component.

Status Configuration

FieldNotes
Default Status*Required text field. Defaults to open in the form. This becomes the status new issues of this type start in.
Allowed Statuses (comma-separated)*Required. Defaults to open,in_progress,closed. The page shows a live preview line: "Currently: open, in_progress, closed". This list is the authoritative set of statuses this issue type accepts.

Custom Fields

Click Add Field to append a field row. Each row has:

Sub-fieldNotes
SlugAuto-seeded as field_{timestamp}, editable.
LabelFree text.
TypeDropdown of exactly ten field types: text, textarea, number, select, multiselect, radio, checkbox, date, rating, file.
OptionsOnly shown when Type is select, radio, or multiselect — rendered by the shared OptionsEditor component (value/label pairs).
RequiredCheckbox.

Click Remove on a field row to delete it before submitting.

Click Create Issue Type (disabled until Slug, Display Name, and Default Status are all filled) to submit. On success you are redirected to /admin/issues/types.

Expected result: New row in issue_types with is_active: true, an audit event issue_type.created logged with the slug in metadata.

Role required: admin (requireRole("admin", { module: "issues" }) on both createIssueTypeAction and updateIssueTypeAction).

Editing an issue type

The Issue Types list page renders an Edit link to /admin/issues/types/{id} for every type, but no page exists at that route in the current build — clicking it will 404. updateIssueTypeAction exists in the module and can update display name, description, icon, default status, allowed statuses, fields, config, and isActive, but there is no UI form wired to call it yet.


Creating an issue

Path: /issues+ Create Issue button → /issues/new

The form loads all active issue types and the assignable staff/admin user list, then presents:

FieldRequiredNotes
Issue TypeYes (dropdown, defaults to the first type returned)Populated from listIssueTypesAction(false) — all active types.
TitleYesMax 500 characters.
DescriptionNoMax 5000 characters.
PriorityNoDropdown: Low, Medium, High, Critical. Defaults to Medium.
Assigned ToNoDropdown of assignable users, or "Unassigned".
Due DateNoDate input.
Tags (comma-separated)NoParsed as JSON if valid, otherwise split on commas and trimmed.

Note: the create form does not render the issue type's custom fields — there is no dynamic field renderer on /issues/new. fieldData can only be populated by direct action calls, not through this UI form.

Click Create Issue. On success you are redirected to /issues/{id}.

Expected result: New row in issues with status set to the issue type's defaultStatus, an audit event issue.created logged with title and typeId in metadata.

Role required: staff (requireRole("staff", { module: "issues" })).


Working an issue

Path: /issues → click a row → /issues/{id}

The detail page has three tabs: Details, Comments (n), History.

Details tab

Shows description (if present), a Custom Fields card listing every key in fieldData as key: value pairs (raw, unlabeled — the type's field definitions are not used to render labels here), and a sidebar with:

  • Update Status — a dropdown fixed to five values (Open, In Progress, Resolved, Closed, On Hold) plus an Update Status button, disabled while the selected value equals the current status.
  • A summary card: Status (via StatusBadge), Assigned To (raw user ID, unformatted — no name lookup), Due Date, Tags, and Created timestamp.

Important: the status dropdown on this page always offers the same five hardcoded values, regardless of the issue type's configured allowedStatuses. If the type's allowedStatuses does not include the selected value, the server rejects the transition (see Troubleshooting below).

Comments tab

Lists all comments (oldest first) with author ID (truncated to 8 characters) and timestamp. A form at the bottom posts a new comment (content, required). Comments created through this form default to commentType: "comment".

Status transitions automatically insert a status_change comment (see below), so the Comments tab doubles as a partial history of status changes for that issue.

History tab

Placeholder only: "Issue history is tracked in the audit log." There is no in-app rendering of the audit trail on this page — an admin must query audit_events directly.

Role required: staff for viewing, transitioning, and commenting (getIssueAction, transitionIssueAction, addCommentAction, listCommentsAction all call requireRole("staff", ...)). Archiving requires lead (see below).


Status values

The issues UI (/issues filter bar and the /issues/{id} Update Status dropdown) hardcodes five status values:

ValueLabel
openOpen
in_progressIn Progress
resolvedResolved
closedClosed
on_holdOn Hold

These are UI defaults, not a database enum — the actual set of statuses an issue can hold is whatever the owning issue type's allowedStatuses array contains (set at issue-type creation, see above). If an admin configures a type with different statuses, the fixed dropdown values will not match, and any mismatched selection will be rejected server-side.

How a status transition is validated

  1. Staff submits a new status via the Update Status form.
  2. transitionIssueAction calls services.transitionStatus(id, { status, notes }, userId).
  3. The service loads the issue's owning issueType and checks issueType.allowedStatuses.includes(input.status).
  4. If not allowed, it throws: Status '{status}' not allowed for this issue type.
  5. If allowed, the issue's status column is updated, a status_change comment is created (content defaults to Status changed from {old} to {new} if no notes were supplied), and an audit event issue.status_changed is logged with fromStatus / toStatus.

Archiving an issue

There is no archive button visible in the current UI pages reviewed (/issues, /issues/{id}). The underlying capability exists: archiveIssueAction(id) sets is_archived = true via a soft delete and logs an issue.archived audit event. Archived issues are excluded from listIssuesAction unless includeArchived: true is passed.

Role required: lead (requireRole("lead", { module: "issues" })).


Filtering the issues list

Path: /issues

A filter bar at the top filters by status only, using the five hardcoded values above (Open, In Progress, Resolved, Closed, On Hold). The page header shows "{filtered} of {total} issues". listIssuesAction also supports server-side filtering by typeSlug, assignedTo, linkedEntityType, linkedEntityId, and includeArchived, but only the status filter is wired to the /issues page UI.


Roles and permissions

ActionRole required
Create issue typeadmin
Update issue typeadmin
List / get issue typesNo role check (public within the authenticated app)
Create issuestaff
Update issuestaff
Get / list issuesstaff
Transition issue statusstaff
Archive issuelead
Add commentstaff
List commentsstaff

Best practices

  1. Set allowedStatuses deliberately at type creation. The Update Status dropdown in the UI is fixed to open, in_progress, resolved, closed, on_hold — if your type's allowed set differs, staff will hit rejected transitions. Match your type's configured statuses to that fixed set unless you are prepared for mismatches.

  2. Keep custom field slugs stable. fieldData is a JSON blob keyed by field slug; renaming a slug after issues exist orphans the old data under a key the current field definitions no longer recognize.

  3. Use tags for cross-cutting classification. Tags are freeform comma-separated (or JSON-array) strings independent of the issue type, useful for filtering across types.

  4. Add notes when transitioning status. The notes field becomes the status_change comment content; without it, the comment defaults to a generic "Status changed from X to Y" line.


Warnings

There is no edit UI for issue types. The types list links to /admin/issues/types/{id} for editing, but no page exists at that route — the link 404s. Changes to an existing type currently require direct action calls or a database update.

The create-issue form does not expose custom fields. Custom fieldData values are not collectable through /issues/new; the form only submits title, description, priority, assignedTo, dueDate, and tags.

The status dropdown does not read the issue type's allowedStatuses. It is hardcoded to five values across the /issues filter bar and the /issues/{id} Update Status control. A type configured with a non-matching status set will reject transitions attempted from the fixed dropdown.

Archived issues have no visible restore path. Archiving is a soft delete (is_archived = true); there is no unarchive action in the reviewed code.


Troubleshooting

"Status '{status}' not allowed for this issue type"

Symptom: Clicking Update Status on /issues/{id} fails with this error message.

Cause: The selected status value is not present in the owning issue type's allowedStatuses array. This commonly happens because the Update Status dropdown always offers the same five fixed values (open, in_progress, resolved, closed, on_hold), regardless of what the type was actually configured with.

Fix: An admin must update the issue type's allowedStatuses to include the desired status (currently only possible via direct service/database update — no edit UI exists), or staff should only select statuses known to be in that type's configured set.


"Failed to create issue type" / "Failed to create issue"

Symptom: Generic error banner on /admin/issues/types/new or /issues/new after submitting.

Cause: Server-side Zod validation failed (e.g., missing required field, slug/title exceeding max length) or the role check in requireRole rejected the current user.

Fix: Confirm all required fields are filled (Display Name, Slug, Default Status for issue types; Title for issues) and that the signed-in user has the admin role (for issue types) or staff+ role (for issues).


Edit link on Issue Types page does nothing / 404s

Symptom: Clicking "Edit" next to an issue type in /admin/issues/types results in a 404 or blank page.

Cause: No page exists at /admin/issues/types/[id] in the current build. Only the "new" creation route is implemented.

Fix: Not resolvable via the UI today. Contact engineering to build the edit page, or make the change directly via updateIssueTypeAction / the database.


FAQ

Can I add custom fields when creating an issue? No. The custom field definitions live on the issue type, but the /issues/new form does not render or submit them. fieldData can only be set through direct action calls.

What is the difference between the issues tracker and the feedback/bugs area? Not established in the issues module code reviewed — no import or reference to the feedback module was found there. Treat them as separate systems unless a dedicated guide states otherwise.

Who can delete an issue? No one, through the UI. Issues are soft-deleted only (archiveIssue sets is_archived = true), and that action requires the lead role. There is no hard-delete path.

Can an issue link to another record in the platform (e.g., an opportunity or organization)? The data model supports it — linkedEntityType and linkedEntityId fields exist on Issue, and listIssuesAction can filter by them — but the reviewed create and detail pages do not expose UI for setting or displaying these fields.

What happens to comments when I change an issue's status? A status_change comment is automatically created with the old and new status, using your notes as the content if provided, or a generic message otherwise.


Related articles

  • The investment pipeline lifecycle — a comparable stage/gate model for opportunities, for contrast with the issues tracker's simpler status model.
  • Guides for forms and form builder, and for approval chains and workflow states, are planned (./forms-and-form-builder.md, ./approval-chains-and-workflow-states.md).