Tickets and boards

This guide covers day-to-day work with tickets in Iskue: creating them, structuring them with sub-tickets, following them, discussing them, and moving them across the kanban board, the backlog and sprints. Every ticket has a sequential key of the form PROJ-123 (allocated race-safely per project) and belongs to exactly one project. Examples assume the full-stack deployment at http://localhost:8088, a project with key CORE, and a shell variable $TOKEN holding your JWT. The full REST surface is browsable at /swagger-ui.html on the backend itself — the frontend proxy on port 8088 does not forward the Swagger paths (see API & webhooks for how to reach them).

Creating tickets

There are four ways to create a ticket: 1. Create button on the project page — opens a modal with the dynamic create form; the same form is available full-page at /projects/{key}/new via Open as page. 2. Board quick-add — an input at the foot of each kanban column (shown only when the board has no swimlanes). It creates a ticket with the project's TASK type (or the first non-subtask type) and, if needed, transitions it straight into that column's status. 3. Sheet view add-row — an Excel-style row at the foot of the sheet grid; type a title, pick a type, press Enter. 4. REST API — POST /api/v1/projects/{projectKey}/tickets.

Ticket types. Each project carries its own set of ticket types (Epic, Story, Task, Bug and Sub-task by default; custom types can be added from the shared catalogue). Types sit at hierarchy levels, which govern parent/child nesting, and each type resolves its own create-form layout, so switching the type in the form re-resolves the fields shown. Subtask types are excluded from the sheet add-row because a subtask needs a parent the sheet has no context to pick.

The create form. The form is driven by the resolved CREATE screen for the chosen type: sections, field order, help text, defaults and required flags all come from the screen configuration. Required fields are marked with *. Conditional rules can show, hide or require fields based on other values, and cascading selects narrow a child field's options to those its parent value allows. If the project defines templates, picking one pre-fills the type, summary, description and priority.

Required-ness is enforced server-side from the same resolved layout, so the API reaches the same verdict as the form: a field hidden by a rule is neither defaulted nor required, defaults are applied to visible fields you left empty, and a still-empty required field rejects the create with '<label>' is required. On edit, writes to read-only fields are rejected and required fields cannot be cleared.

Custom fields

Projects can define custom fields of eight types. Values are validated on every create and update; a custom field marked required must be present on create and cannot be cleared later (Custom field '<name>' is required and cannot be cleared). In API payloads, custom values are passed as a customValues map keyed by the field's id.

TypeHoldsAccepted value
TEXTFree textNon-empty string
NUMBERA numberJSON number
DATEA dateISO date string, yyyy-MM-dd
SELECTOne optionString matching one of the field's options
MULTI_SELECTSeveral optionsList of strings, each a configured option
CHECKBOXYes/notrue or false
USERA personUser id (UUID) of an existing user
URLA linkString starting http:// or https://
Create a ticket. "type" accepts EPIC, STORY, TASK or BUG; pass "typeId" (a project ticket-type UUID) for subtask or custom types. Priorities: HIGHEST, HIGH, MEDIUM, LOW, LOWEST. "estimate" is story points, 0–1000. The creator becomes the reporter and automatically watches the ticket.
curl -s -X POST http://localhost:8088/api/v1/projects/CORE/tickets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"TASK","title":"Tune the connection pool","priority":"HIGH","estimate":3}'

Sub-tickets and rollup

A ticket becomes a sub-ticket by carrying a parentId (set on create, or later via PATCH /api/v1/tickets/{ticketKey} — pass clearParent: true to detach). Two rules are enforced: parent and child must belong to the same project, and the parent's type must sit strictly higher in the type hierarchy than the child's — a Story can parent a Task, but not another Story. Violations are rejected with a conflict such as A 'Task' cannot be parented under a 'Task': the parent's type must be higher in the hierarchy.

The ticket detail page shows the parent as a link, and on the parent a Children section lists every sub-ticket with a rollup progress bar: done/total tickets and donePoints/totalPoints story points, where *done* means the child's status is in the DONE category. In this rollup an unestimated child counts as 1 point so it still moves the bar. Epics additionally appear on the epics roadmap with the same child rollup.

Returns { total, done, totalPoints, donePoints, children: [...] }.
curl -s http://localhost:8088/api/v1/tickets/CORE-10/children \
  -H "Authorization: Bearer $TOKEN"

Watching

Use the Watch button on the ticket detail page to follow a ticket; it shows the current watcher count and toggles to unwatch. The reporter is added as a watcher automatically at creation. Under the default notification scheme, watchers are notified (alongside the reporter and current assignee) when the ticket changes status and when someone comments; notifications arrive in the in-app inbox, and by email where the email channel is enabled and the recipient has not opted out.

curl -s http://localhost:8088/api/v1/tickets/CORE-42/watch -H "Authorization: Bearer $TOKEN"   # {"watching":...,"watcherCount":...}
curl -s -X POST   http://localhost:8088/api/v1/tickets/CORE-42/watch -H "Authorization: Bearer $TOKEN"   # start watching
curl -s -X DELETE http://localhost:8088/api/v1/tickets/CORE-42/watch -H "Authorization: Bearer $TOKEN"   # stop watching

Comments, @mentions and reactions

Comments support Markdown and thread one level deep: replying to a reply attaches the new comment to the thread's top-level parent. Ticking Internal note marks a comment agent-only — customers never see it on the portal — while a public comment on a portal-raised ticket also notifies the requester. Only the author can edit a comment body; edits are marked and logged in the ticket history. Comments can be resolved and reopened (by the author, or anyone with EDIT_TICKET). Deleting your own comment needs DELETE_OWN_COMMENT; deleting someone else's needs DELETE_ALL_COMMENTS, and a comment with replies refuses deletion until the replies are removed first.

@mentions work in comment bodies and ticket descriptions. Typing @ at a word boundary opens a member autocomplete; picking someone inserts @handle or, for names with spaces, @"Display Name". A bare handle matches a project member case-insensitively by email, the email's local part, the display name, or the display name with spaces removed. Resolution is scoped to the project's members; unknown or ambiguous handles resolve to nobody. Mentioned members receive a MENTIONED notification — only *newly added* mentions notify when text is edited, and you are never notified for mentioning yourself.

Reactions use a fixed palette — 👍 👎 🎉 ❤️ 😄 🚀 — rendered as toggle chips with per-emoji counts under each comment. Clicking a chip adds or removes your reaction; any project member who can see the ticket may react. Emoji outside the palette are rejected with Unsupported reaction.

Add a comment with a quoted @mention. Pass "parentId" (a comment UUID) to reply in a thread.
curl -s -X POST http://localhost:8088/api/v1/tickets/CORE-42/comments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":"@\"Priya Sharma\" could you take a look?","internal":false}'

The kanban board

The project page opens on the Board view (a switcher also offers List, Table and Sheet). A project can have several boards; the board selector picks between them. Each board maps its columns to one or more workflow statuses and can carry per-board settings — configured from the board panel on non-default boards: columns (name, statuses, WIP limit), a swimlane field, a TQL scope query that filters which tickets appear, and the card fields to display (labels, type, assignee, priority, due, estimate — the default set is labels, type, assignee and priority). Cards also edit assignee and priority inline via dropdowns.

Dragging a card between columns is a status transition. Dropping onto a column transitions the ticket to that column's first (primary) status. The move is optimistic in the UI but validated by the workflow engine on the server: if the transition is not defined from the current status, or a transition condition or validator blocks it, the card snaps back and the error is shown (for example Transition to 'Done' is not allowed from the current status). GET /api/v1/tickets/{ticketKey}/transitions lists the targets currently legal for you on a given ticket.

  • WIP limits — a column with a limit shows count/limit in its header. When the count exceeds the limit the column is highlighted and flagged ⚠ *WIP limit exceeded*. The count spans all swimlanes, and the limit is advisory: drops are flagged, never blocked.
  • Swimlanes — group the board into rows by Assignee, Type, Priority or Epic (the parent ticket's key). Placeholder lanes (*Unassigned*, *No epic*) sort last. Column quick-add is hidden while swimlanes are active.
  • Scope — a board with a TQL scope (for example type = BUG AND status != Done) shows only matching tickets.

Live updates over SSE

Open board and ticket pages stay current without refreshing. Each project exposes a Server-Sent Events stream at GET /api/v1/projects/{projectKey}/live (project members only). Subscribers receive a connected event, then board events whenever anything board-relevant changes (create, update, transition, rank move, sprint change) and ticket events carrying the affected ticket key. The frontend refetches on board events; if the connection drops, the browser's EventSource reconnects automatically. Events are fanned out across backend nodes, so any replica's mutation reaches every subscriber.

Browser EventSource clients cannot set headers; they pass the JWT as an access_token query parameter instead.
curl -N http://localhost:8088/api/v1/projects/CORE/live \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream"

Backlog ranking

The backlog lives at /projects/{key}/backlog and lists every ticket not assigned to a sprint, in rank order. Reorder with the ▲▼ move controls on each row; each move sends the ticket's new neighbours to the rank endpoint, which computes a rank string between them. If the list changed under you and the neighbours leave no room, the move is rejected with Cannot reorder here — the list changed; please refresh and try again — refresh and retry. The backlog can also be grouped by status, assignee, priority or type (sticky per project); the move controls hide while a grouping is active, since rank order only makes sense ungrouped. A nightly job (03:00 server time) rewrites a project's ranks as short, evenly spaced strings whenever repeated mid-point insertion has grown any rank beyond 20 characters — order is preserved.

Place CORE-42 between CORE-40 and CORE-41. Pass null for previousTicketKey (top) or nextTicketKey (bottom).
curl -s -X POST http://localhost:8088/api/v1/tickets/CORE-42/rank \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"previousTicketKey":"CORE-40","nextTicketKey":"CORE-41"}'

Sprints

Sprints are created from the backlog page with a name and an optional goal, and move through three states: PLANNED → ACTIVE → COMPLETED. Assign tickets by picking a sprint in the dropdown on any backlog row (or via the API); a completed sprint refuses new tickets. Start works only on a planned sprint and only when no other sprint is active in the project — the guard rejects with Another sprint is already active in this project. Starting stamps today as the start date and accepts an optional end date. Complete works only on the active sprint: every ticket whose status category is not DONE is returned to the backlog (its sprint assignment is cleared), and the end date defaults to today if unset. Each open sprint's panel also offers a per-assignee workload table and per-team load-versus-capacity bars. Creating, editing, starting, completing and assigning to sprints all require the MANAGE_SPRINTS permission.

Sprint lifecycle. The sprint UUID comes from the create response or GET /api/v1/projects/CORE/sprints.
# create a sprint
curl -s -X POST http://localhost:8088/api/v1/projects/CORE/sprints \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Sprint 12","goal":"Ship the importer"}'

# pull a ticket into it (null sprintId returns the ticket to the backlog)
curl -s -X POST http://localhost:8088/api/v1/tickets/CORE-42/sprint \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"sprintId":"<sprint-uuid>"}'

# start and, later, complete
curl -s -X POST http://localhost:8088/api/v1/projects/CORE/sprints/<sprint-uuid>/start \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"endDate":"2026-08-17"}'
curl -s -X POST http://localhost:8088/api/v1/projects/CORE/sprints/<sprint-uuid>/complete \
  -H "Authorization: Bearer $TOKEN"

The Sheet view

The Sheet view (project page → Sheet) is an Excel-style grid over the project's tickets. Every cell except the key edits in place: press Enter or F2 (or double-click) to open the cell editor, Enter commits, Escape cancels, and clicking away also commits. Arrow keys and Tab move a roving focus through the grid. Text, number and date cells update optimistically and roll back if the server rejects the change; select cells (type, status, priority, assignee) apply the server's response exactly, and editing the status cell performs a real workflow transition, so an illegal target fails just as a board drag would. Column widths drag-resize on the header handles and persist per project, and the header stays sticky while the grid scrolls. After the core columns, the sheet appends one column per project custom field — all edit inline except MULTI_SELECT, which stays display-only here (edit it on the ticket detail page). The bottom add row creates tickets without leaving the grid: type a title, pick a type (non-subtask types only, defaulting to TASK), press Enter, and focus returns to the title input so rows can be entered one after another. A group-by selector and saved table views apply their grouping and sort to the sheet as well.

Sheet edits go through the same PATCH and transition endpoints as every other surface, so screen rules still apply: a read-only field rejects the write, a required field cannot be cleared, and workflow validation governs the status column. When a cell edit fails, the error is shown and the cell rolls back — nothing is silently dropped.

API quick reference

Method and pathPurpose
POST /api/v1/projects/{projectKey}/ticketsCreate a ticket
GET /api/v1/projects/{projectKey}/ticketsList tickets — filters: statusId, typeId, assigneeId, labelId, sprintId, backlog, q; sort=rank for backlog order
GET / PATCH /api/v1/tickets/{ticketKey}Read / update a ticket
POST /api/v1/tickets/{ticketKey}/transitionChange status ({"statusId":...}), workflow-validated
GET /api/v1/tickets/{ticketKey}/transitionsStatuses currently reachable for this user and ticket
POST /api/v1/tickets/{ticketKey}/rankReorder between two neighbour keys
GET /api/v1/tickets/{ticketKey}/childrenSub-tickets with rollup totals
GET / POST / DELETE /api/v1/tickets/{ticketKey}/watchWatch state / watch / unwatch
GET / POST /api/v1/tickets/{ticketKey}/commentsList / add comments
POST /api/v1/tickets/{ticketKey}/comments/{commentId}/reactionsToggle a reaction ({"emoji":"👍"})
POST /api/v1/tickets/{ticketKey}/sprintAssign to a sprint ({"sprintId":null} clears)
GET / POST /api/v1/projects/{projectKey}/sprintsList / create sprints; POST .../{sprintId}/start and .../{sprintId}/complete drive the lifecycle
GET /api/v1/projects/{projectKey}/liveServer-Sent Events stream (board and ticket events)