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.

CodeDefault nameDefault levelSub-taskSeeded description
EPICEpic1NoA large body of work that can be broken into stories
STORYStory0NoA user-facing feature or requirement
TASKTask0NoA unit of work
BUGBug0NoA problem or defect
SUBTASKSub-task-1YesA 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} with name sets 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 PATCH accepts level; it changes only this project's placement of the type.
  • Reorder — POST …/ticket-types/reorder with {"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).

EndpointAccessPurpose
GET /api/v1/projects/{projectKey}/ticket-typesMemberList the project's types in display order
GET …/ticket-types/hierarchyMemberTypes grouped into level tiers, highest first
GET …/ticket-types/assignableProject adminCatalogue entries not yet added to the project
POST …/ticket-typesProject adminCreate a new type (catalogue entry + association)
POST …/ticket-types/associateProject adminAdd existing catalogue types to the project
PATCH …/ticket-types/{id}Project adminPer-project rename override and level
POST …/ticket-types/reorderProject adminSet display order
DELETE …/ticket-types/{id}Project adminRemove 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. The custom flag 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

fieldRefLabelControlPlacement rules
typeTypeTICKET_TYPEMandatory on CREATE; required by default
summarySummaryTEXTMandatory on every layout; can never be hidden; required by default
descriptionDescriptionTEXTAREA—
statusStatusSTATUSExcluded from CREATE (the workflow sets the initial status)
priorityPriorityPRIORITYOptions: HIGHEST, HIGH, MEDIUM, LOW, LOWEST
assigneeAssigneeUSER—
reporterReporterUSER—
parentParentPARENT—
sprintSprintSPRINT—
estimateEstimateNUMBER—
labelsLabelsLABELS—

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.

PUT /api/v1/projects/{projectKey}/screens/CREATE?typeId={typeId} — replace the CREATE layout for one type
{
  "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).

A rule entry in the layout's "rules" array: require the field while priority is Highest or High
{
  "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.

PUT /api/v1/projects/{projectKey}/screens/dependencies — replace the project's cascading-select dependencies
{
  "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).

EndpointAccessPurpose
GET …/screens/resolve?operation=&typeId=MemberThe resolved layout the forms render
GET …/screens/catalog?operation=Project adminPlaceable fields for the builder palette
PUT …/screens/{operation}?typeId=Project adminReplace a layout (omit typeId for the project default)
DELETE …/screens/{operation}?typeId=Project adminReset the context to the synthesised default
GET / PUT …/screens/dependenciesProject adminRead / 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.

TypeAccepted valueValidation
TEXTstringNon-empty
NUMBERnumberMust be a JSON number
DATEstringISO date, yyyy-MM-dd
SELECTstringMust be one of the field's options
MULTI_SELECTarray of stringsEvery entry must be one of the field's options
CHECKBOXbooleantrue or false
USERstringUUID of an existing user
URLstringMust 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.

POST /api/v1/admin/field-schemes — define a scheme once, then apply it per project
{
  "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.

POST /api/v1/projects/{projectKey}/templates — the ticket is pre-filled from title/description/priority (and typeId when set)
{
  "name": "Weekly ops review",
  "title": "Ops review",
  "description": "Checklist:\n- dashboards\n- alerts\n- capacity",
  "priority": "MEDIUM",
  "recurrence": "WEEKLY"
}