Screens, fields and ticket types
Iskue resolves every ticket form from configuration rather than fixed markup: which types a project offers, which fields appear on the create and edit forms, in what order, with what defaults, and under which conditions. This page covers the ticket-type catalogue, dynamic screens (layouts, conditional rules, cascading selects), custom fields, reusable field schemes and ticket templates. Throughout the screen model, a field is addressed by a single string fieldRef: a system key such as summary or priority, or custom:{id} for a custom field, where {id} is the field's UUID.
Ticket types
Ticket types are split into a global catalogue and per-project associations. The catalogue (ticket_type_catalog) holds each type's shared identity: a stable, globally unique code, the default name, description, icon, whether it is a sub-task, and a default hierarchy level. A project does not own types — it *associates* with catalogue entries (project_ticket_type), and the association carries the per-project knobs: hierarchy level, sort_order and an optional name_override. A ticket's type_id points at the association, so the same catalogue type can be renamed and sit at different hierarchy tiers in different projects. Every new project is automatically associated with the five built-in system types, in the order shown below.
| Code | Default name | Default level | Sub-task | Seeded description |
|---|---|---|---|---|
| EPIC | Epic | 1 | No | A large body of work that can be broken into stories |
| STORY | Story | 0 | No | A user-facing feature or requirement |
| TASK | Task | 0 | No | A unit of work |
| BUG | Bug | 0 | No | A problem or defect |
| SUBTASK | Sub-task | -1 | Yes | A small unit of work under another issue |
Hierarchy levels. Each type in a project sits at an integer level; higher means higher in the hierarchy. The convention is Epic at 1, standard work items at 0, sub-tasks at -1, but any integer is accepted, so extra tiers (an "Initiative" at 2, say) are possible. The rule enforced on every parent link is strict: the parent's type level must be greater than the child's — a Story (level 0) can be parented under an Epic (level 1) but not under another Story. Violations are rejected with a conflict error. Tickets whose type cannot be resolved (legacy data) are exempt, so existing links are never retroactively broken. GET /api/v1/projects/{projectKey}/ticket-types/hierarchy returns the project's types grouped into tiers, highest level first. The default type for new tickets is the first non-sub-task type in project order.
- Rename per project —
PATCH …/ticket-types/{id}withnamesets a project-local override; the catalogue name is untouched and other projects are unaffected. A blank name (or renaming back to the exact catalogue name) clears the override. Effective display names are unique within a project, case-insensitively. - Move between tiers — the same
PATCHacceptslevel; it changes only this project's placement of the type. - Reorder —
POST …/ticket-types/reorderwith{"orderedIds": [...]}sets the display order. - Remove from the project —
DELETE …/ticket-types/{id}detaches the association only; the catalogue entry remains for other projects. Removal is blocked (HTTP 409) while any ticket in the project still uses the type, and a project must always keep at least one type.
Custom types. "Create new" (POST …/ticket-types) defines a new catalogue entry org-wide and associates it with the project in one step: name (max 60 characters, required), optional description (max 255), icon (max 40), subtask flag and level (defaults to -1 for sub-task types, else 0). The stable code is derived from the name — upper-cased, non-alphanumerics stripped, truncated to 30 characters, with a numeric suffix appended if the code is already taken. "Add existing" is a two-step flow: GET …/ticket-types/assignable lists catalogue entries not yet in the project, and POST …/ticket-types/associate with {"catalogIds": [...]} adds them at their catalogue default level (idempotent — already-associated ids are skipped).
| Endpoint | Access | Purpose |
|---|---|---|
GET /api/v1/projects/{projectKey}/ticket-types | Member | List the project's types in display order |
GET …/ticket-types/hierarchy | Member | Types grouped into level tiers, highest first |
GET …/ticket-types/assignable | Project admin | Catalogue entries not yet added to the project |
POST …/ticket-types | Project admin | Create a new type (catalogue entry + association) |
POST …/ticket-types/associate | Project admin | Add existing catalogue types to the project |
PATCH …/ticket-types/{id} | Project admin | Per-project rename override and level |
POST …/ticket-types/reorder | Project admin | Set display order |
DELETE …/ticket-types/{id} | Project admin | Remove from the project (guarded) |
Editing or deleting a shared catalogue definition affects every project, so it is reserved for organisation administrators (role ADMIN) under /api/v1/admin/ticket-type-catalog (list, create with an optional explicit code, patch name/description/icon/subtask/defaultLevel — code is immutable — and delete). System types can never be deleted, and a catalogue entry cannot be deleted while any project still associates it.
Dynamic screens
A screen is the field layout for one form context: (project × ticket type × operation), where the operation is CREATE or EDIT (the API still accepts a legacy VIEW operation for compatibility, but no runtime surface renders it — the ticket detail's read state follows the EDIT screen). Each layout is an ordered list of named sections, each holding ordered fields with per-field settings. The create form and the ticket detail page both render from a resolved layout (GET …/screens/resolve) rather than fixed markup, and the admin screen designer's live preview uses the same renderer, so preview and runtime match.
- Resolution order for
resolve(project, typeId, operation): 1. a screen-scheme mapping for the type (then the project-default scheme mapping); 2. the direct layout saved for exactly this (project, type, operation); 3. the project-default layout for the operation (saved with no type); 4. a synthesised built-in default derived from the project's current fields. Thecustomflag in the response tells you whether a persisted layout (true) or the synthesised default (false) was used. - Because the last step always succeeds, an unconfigured project works with zero screen rows — nothing needs to be set up before tickets can be created.
Synthesised defaults. For CREATE: a single "Create" section with type, summary, description, priority, assignee. For EDIT: a "Details" section (type, status, priority, assignee, parent, sprint, estimate), a "Labels" section, a "Fields" section listing all of the project's custom fields in their sort order (present only when custom fields exist), and a "Description" section. Synthesised defaults carry no conditional rules. The ticket detail's read state renders the resolved EDIT layout with every field read-only.
System fields
| fieldRef | Label | Control | Placement rules |
|---|---|---|---|
type | Type | TICKET_TYPE | Mandatory on CREATE; required by default |
summary | Summary | TEXT | Mandatory on every layout; can never be hidden; required by default |
description | Description | TEXTAREA | — |
status | Status | STATUS | Excluded from CREATE (the workflow sets the initial status) |
priority | Priority | PRIORITY | Options: HIGHEST, HIGH, MEDIUM, LOW, LOWEST |
assignee | Assignee | USER | — |
reporter | Reporter | USER | — |
parent | Parent | PARENT | — |
sprint | Sprint | SPRINT | — |
estimate | Estimate | NUMBER | — |
labels | Labels | LABELS | — |
Saving a layout validates it: every fieldRef must exist (a system key or a custom:{id} belonging to the project), a field may appear only once per layout, summary must be present on every CREATE and EDIT layout, type must be present on CREATE, and status may not be placed on CREATE. Only type and summary are required by default; custom fields default to their own required flag. GET …/screens/catalog?operation= returns the placeable fields for the builder palette, marking which are removable for that operation.
Sections, field order, defaults and required flags
Each field slot on a layout carries four optional settings: requiredOverride (true/false; null inherits the field's own required-ness, so the same custom field can be required on the Bug create screen and optional on Task), readOnly (rejects edits with HTTP 409), defaultValue (stored as JSON; applied on CREATE to visible fields the submitter left empty, after which required-ness is checked), and helpText (max 255 characters, shown under the input). PUT …/screens/{operation}?typeId={id} replaces the whole layout for that context — omit typeId to edit the project default. DELETE on the same path removes the custom layout and reverts the context to the synthesised default.
{
"sections": [
{
"name": "Details",
"fields": [
{ "fieldRef": "type" },
{ "fieldRef": "summary", "helpText": "One line: what breaks, and where" },
{ "fieldRef": "description" },
{ "fieldRef": "priority", "defaultValue": "MEDIUM" },
{ "fieldRef": "custom:6f8d2c1a-4b9e-4c2d-9f10-3a5e8b7c0d21", "requiredOverride": true }
]
}
],
"rules": []
}Conditional rules: show, hide, require
A rule targets a field (targetKind: "FIELD", targetRef = its fieldRef) or a whole section (targetKind: "SECTION", targetRef = the section name) and applies an effect while its predicate holds. SHOW hides the target unless the predicate matches (several SHOW rules on one target: visible if any matches); HIDE hides it when the predicate matches, and wins over SHOW; REQUIRE makes a visible field required. A hidden field — directly or via its section — is neither required, defaulted nor validated. HIDE may never target summary, and a FIELD rule must target a field that is actually on the layout. The predicate is {"any": [ {"all": [ condition… ]} ]} — OR across groups, AND within a group; an empty or absent predicate always matches. Operators: EQUALS, NOT_EQUALS, IN, NOT_IN (EQUALS/NOT_EQUALS compare against a single value; IN/NOT_IN take a values list), IS_EMPTY, IS_NOT_EMPTY, GREATER_THAN, LESS_THAN (numeric when both sides parse as numbers, else lexicographic), and CONTAINS (membership for multi-selects, substring for text).
{
"targetRef": "custom:6f8d2c1a-4b9e-4c2d-9f10-3a5e8b7c0d21",
"targetKind": "FIELD",
"effect": "REQUIRE",
"predicate": {
"any": [
{ "all": [ { "fieldRef": "priority", "op": "IN", "values": ["HIGHEST", "HIGH"] } ] }
]
}
}Rules are evaluated live in the browser on every change, and re-evaluated on the server at submit time by a mirrored evaluator kept in behavioural parity with the client one, so hiding a required field client-side cannot bypass validation. On create, the server resolves the CREATE layout, applies defaults, then rejects any visible required field that is still empty; on edit it resolves the EDIT layout, rejects writes to read-only fields and refuses to clear a required field. Rules exist only on persisted (custom) layouts.
Cascading selects
A field dependency makes one custom SELECT/MULTI_SELECT field constrain another: the parent's chosen value filters the child's visible options through an explicit mapping of parent option → allowed child options. Both parent and child must be custom select fields (system enums cannot cascade); a field cannot depend on itself, and at most one dependency exists per parent/child pair. The behaviour is deliberately narrow-only: a selected parent value with no entry in the mapping leaves the child unconstrained (all options allowed), and with a multi-select parent the allowed set is the union across its selected values. Dependencies are project-scoped and apply on every form where both fields appear. The server re-validates on ticket create and update: a submitted child value that the current parent value does not allow is rejected. Mapping keys must be actual parent options and mapped values actual child options.
{
"dependencies": [
{
"parentRef": "custom:aa11d4e0-2c3b-45f6-8a9b-0c1d2e3f4a55",
"childRef": "custom:bb22e5f1-3d4c-56a7-9b0c-1d2e3f4a5b66",
"mapping": {
"Hardware": ["Laptop", "Monitor"],
"Software": ["Licence request"]
}
}
]
}Two adjacent mechanisms share this model. Transition screens are named per-project layouts bound to workflow transitions: a small dialog that collects a few fields mid-move (summary, description, priority, assignee, estimate, labels only; nothing is mandatory on them). Screen schemes map operations to named screens and are assigned per type or as the project default; when a scheme mapping exists it wins over the direct per-context binding. Both are managed under the same …/screens API (/transition-screens, /schemes).
| Endpoint | Access | Purpose |
|---|---|---|
GET …/screens/resolve?operation=&typeId= | Member | The resolved layout the forms render |
GET …/screens/catalog?operation= | Project admin | Placeable fields for the builder palette |
PUT …/screens/{operation}?typeId= | Project admin | Replace a layout (omit typeId for the project default) |
DELETE …/screens/{operation}?typeId= | Project admin | Reset the context to the synthesised default |
GET / PUT …/screens/dependencies | Project admin | Read / replace cascading-select dependencies |
Custom fields
Custom fields are defined per project and come in eight types. Each has a name (max 60 characters, unique within the project, case-insensitively), a required flag, a sort order, and — for the two select types — a list of allowed options (at least one is mandatory). Values live on the ticket itself, in the custom_values JSONB column keyed by the field's UUID, so adding a field never rewrites existing tickets. Every submitted value is validated against the field's type; unknown field ids are rejected.
| Type | Accepted value | Validation |
|---|---|---|
TEXT | string | Non-empty |
NUMBER | number | Must be a JSON number |
DATE | string | ISO date, yyyy-MM-dd |
SELECT | string | Must be one of the field's options |
MULTI_SELECT | array of strings | Every entry must be one of the field's options |
CHECKBOX | boolean | true or false |
USER | string | UUID of an existing user |
URL | string | Must start with http:// or https:// |
Fields are managed at …/projects/{projectKey}/fields: members list (GET), project admins create (POST) and delete (DELETE /fields/{fieldId}). There is no update endpoint — a field's name, type, options and required flag are fixed once created; to change them, delete and re-create the field. Deleting a field removes it from resolved layouts automatically (slots referencing it simply disappear) and later submissions naming it are rejected; values already stored on tickets remain in the JSONB but are no longer rendered. Custom fields are project-global rather than per-type — per-type visibility is achieved by placing (or not placing) the field on that type's screens.
Field schemes
A field scheme is a named, org-level set of custom-field definitions — define the set once, apply it to any number of projects. Organisation administrators (role ADMIN) manage schemes under /api/v1/admin/field-schemes (create, replace the field list, delete); any signed-in user can list them at GET /api/v1/field-schemes so project pickers can render. Applying is project-admin: POST /api/v1/projects/{projectKey}/field-schemes/{schemeId}/apply copies the definitions into the project's own custom fields, matching by name, case-insensitively, and idempotently — a field name that already exists in the project is skipped, never overwritten. The response reports {"created": n, "skipped": m}. Because applying copies, later edits to the scheme do not propagate to projects that already applied it; re-applying only adds fields the project is still missing. Scheme names are unique, field names are unique within a scheme, and select-type scheme fields need at least one option.
{
"name": "Support intake",
"description": "Standard intake fields for support projects",
"fields": [
{ "name": "Severity", "type": "SELECT", "required": true, "options": ["S1", "S2", "S3"] },
{ "name": "Customer", "type": "TEXT", "required": false },
{ "name": "Review by", "type": "DATE", "required": false }
]
}Applying a scheme also links each created (and matching pre-existing) project field to a shared org-field identity keyed by name and type, so "Severity" in project A and "Severity" in project B are recognisably the same field. GET /api/v1/admin/org-fields lists identities with their link counts, and POST /api/v1/admin/org-fields/link-existing backfills links for fields that pre-date the feature. This is groundwork for cross-project reporting; per-project values and forms are unaffected.
Ticket templates
A ticket template pre-fills the create form. It stores a name (max 120 characters, unique per project), a title that becomes the ticket summary (max 255), an optional description (max 100 000 characters), an optional priority and an optional ticket type (typeId). Picking a template in the create form fills the type (when the template names one), summary, description and priority; a template without a description clears the description field, and anything already entered in other fields is kept. Members can list templates, any member with write access can create and update them, and deletion requires a project admin (…/projects/{projectKey}/templates). A template may also carry a recurrence of NONE, DAILY, WEEKLY or MONTHLY: recurring templates auto-create a ticket each period (calculated in UTC), and on a multi-node cluster each run is claimed by exactly one node — a crash after a claim skips that run rather than ever creating a duplicate.
{
"name": "Weekly ops review",
"title": "Ops review",
"description": "Checklist:\n- dashboards\n- alerts\n- capacity",
"priority": "MEDIUM",
"recurrence": "WEEKLY"
}