Docs · Developers
Plugin development
Alpha — plugin API 0.3. Java plugins run on Iskue today as an internal alpha. The API is still 0.x, so names and signatures can change before 1.0, and the iskue-plugin-api artefact is not published yet: design partners get the API and a sample plugin on request (get in touch). Uploading plugins is off by default and stays off on Iskue's hosted instances until signed packages ship. Available now is what you can build today; Planned is the rest of the platform.
Jira Data Center lets partners and customers build apps that run inside Jira. Iskue follows the same model: a plugin is one JAR that runs inside Iskue, adds its own REST endpoints, reacts to work-item changes, shows a panel on the work item page, adds workflow conditions, validators and post-functions and TQL functions, keeps its own tables, and works with work items, search, projects and people through a versioned Java API. Field types, background jobs and a marketplace for Java plugins follow. If you have built a Jira Data Center app, the mapping table shows where each concept lands.
Three ways to extend Iskue
| Kind | Runs | Written in | For | Status |
|---|---|---|---|---|
| Java plugins | Inside Iskue, like Jira Data Center apps | Java 21 (plus JavaScript for the UI) | Partners porting Data Center apps, internal platform teams | Alpha — API 0.3, described below |
| Script plugins | In a locked-down JavaScript sandbox inside Iskue | JavaScript | Administrators and customers automating their instance | Available today (Plugins); will gain the same descriptor, read access, storage and allow-listed HTTP |
| Remote apps | On the vendor's own servers | Any language | SaaS vendors | Planned, after Java plugins |
All three will share one descriptor format, one administration console and one marketplace.
Available now: API 0.3
A Java plugin is a JAR with an iskue-plugin.yaml descriptor at its root, built for Java 21. Iskue gives each plugin its own class loader and its own Spring context on every application server. Plugin code sees exactly one Iskue package, com.iskue.plugin.api, plus a short list of shared libraries; the rest of Iskue is invisible to it, so a plugin cannot come to depend on internals by accident.
A first plugin
This plugin lists the work items of a project that nobody has touched for a while, lets project administrators choose how long "a while" is, and nudges the stale ones with a comment. It also raises the priority of any new work item whose summary mentions an outage, and shows the stale list in a panel on the work item page. Its workflow validator and TQL function are shown further down.
src/main/resources/iskue-plugin.yamlkey: acme.stale-work
name: Stale work
version: 1.0.0
description: Finds, nudges and guards work items nobody has touched for a while, and raises new outages.
vendor: { name: Acme, url: https://acme.example }
apiVersion: "[0.3,1.0)" # the plugin API versions it was built for
runtime: java
scopes: [read:ticket, write:ticket, write:comment, read:project]
modules:
- { type: rest, key: api, class: com.acme.stale.StaleResource }
- { type: event-listener, key: outages, class: com.acme.stale.OutageListener }
- type: ticket-panel
key: stale-panel
entry: web/stale-panel.js#StalePanel
title: { en: Stale work, fr: Travail en sommeil }
- type: workflow-validator
key: needs-description
class: com.acme.stale.NeedsDescription
title: { en: Needs a description, fr: Exige une description }
- type: tql-function
key: stale-in
name: staleIn # used as key IN staleIn(OPS)
class: com.acme.stale.StaleFunction
title: { en: Stale work items of a project, fr: Éléments en sommeil d'un projet }rest module, served under /api/plugins/acme.stale-work/…package com.acme.stale;
import com.iskue.plugin.api.PluginApiException;
import com.iskue.plugin.api.PluginApiException.Reason;
import com.iskue.plugin.api.cluster.ClusterLock;
import com.iskue.plugin.api.permission.PermissionApi;
import com.iskue.plugin.api.project.ProjectApi;
import com.iskue.plugin.api.project.ProjectView;
import com.iskue.plugin.api.search.SearchApi;
import com.iskue.plugin.api.settings.SettingsApi;
import com.iskue.plugin.api.ticket.TicketApi;
import com.iskue.plugin.api.ticket.TicketView;
import java.util.List;
import java.util.Map;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/projects/{projectKey}")
public class StaleResource {
public record Threshold(int days) {
}
private final ProjectApi projects;
private final SearchApi search;
private final TicketApi tickets;
private final SettingsApi settings;
private final PermissionApi permissions;
private final ClusterLock lock;
public StaleResource(ProjectApi projects, SearchApi search, TicketApi tickets,
SettingsApi settings, PermissionApi permissions, ClusterLock lock) {
this.projects = projects;
this.search = search;
this.tickets = tickets;
this.settings = settings;
this.permissions = permissions;
this.lock = lock;
}
/** GET …/projects/OPS/stale: the stale work items' keys, oldest first. */
@GetMapping("/stale")
public List<String> stale(@PathVariable("projectKey") String projectKey) {
return search.search(staleQuery(project(projectKey)), 0, SearchApi.MAX_PAGE_SIZE)
.tickets().stream().map(TicketView::key).toList();
}
/** PUT …/projects/OPS/threshold {"days": 14}: project administrators only. */
@PutMapping("/threshold")
public Threshold setThreshold(@PathVariable("projectKey") String projectKey,
@RequestBody Threshold body) {
ProjectView project = project(projectKey);
if (!permissions.canAdminister(project.key())) {
throw new PluginApiException(Reason.FORBIDDEN, "Only a project administrator can change this.");
}
if (body.days() < 1 || body.days() > 365) {
throw new PluginApiException(Reason.INVALID, "Choose between 1 and 365 days.");
}
settings.set(project.key(), "stale.days", Integer.toString(body.days()));
return body;
}
/** POST …/projects/OPS/nudge: one comment per stale work item, one run at a time in the cluster. */
@PostMapping("/nudge")
public Map<String, Object> nudge(@PathVariable("projectKey") String projectKey) {
ProjectView project = project(projectKey);
int[] nudged = {0};
boolean ran = lock.tryRun("nudge-" + project.key(), () -> {
for (TicketView t : search.search(staleQuery(project), 0, 50).tickets()) {
tickets.comment(t.key(), "Nothing has moved here for a while. Is this still in progress?");
nudged[0]++;
}
});
return Map.of("ran", ran, "nudged", nudged[0]);
}
private ProjectView project(String key) {
return projects.get(key)
.orElseThrow(() -> new PluginApiException(Reason.NOT_FOUND, "No project " + key));
}
private String staleQuery(ProjectView project) {
int days = settings.get(project.key(), "stale.days").map(Integer::parseInt).orElse(30);
return "project = " + project.key() + " AND statusCategory != Done AND updated < -" + days + "d"
+ " ORDER BY updated ASC";
}
}event-listener modulepackage com.acme.stale;
import com.iskue.plugin.api.event.TicketEvent;
import com.iskue.plugin.api.event.TicketEventListener;
import com.iskue.plugin.api.ticket.TicketApi;
import com.iskue.plugin.api.ticket.TicketUpdate;
/** Raises the priority of a new work item whose summary mentions an outage. */
public class OutageListener implements TicketEventListener {
private final TicketApi tickets;
public OutageListener(TicketApi tickets) {
this.tickets = tickets;
}
@Override
public void onTicketEvent(TicketEvent event) {
if (!"CREATED".equals(event.kind())) {
return; // UPDATED, STATUS_CHANGED, ASSIGNED, COMMENTED, … — ignore kinds you do not know
}
tickets.get(event.ticketKey())
.filter(t -> t.summary().toLowerCase().contains("outage"))
.filter(t -> !"HIGHEST".equalsIgnoreCase(t.priority()))
.ifPresent(t -> tickets.update(t.key(), TicketUpdate.none().withPriority("HIGHEST")));
}
}There is no registration code: every module class named in the descriptor becomes a Spring bean, and the API services it needs arrive through its constructor. List your own packages under scan to have further @Component classes picked up. The panel is shown below.
The descriptor
| Field | Required | Meaning |
|---|---|---|
key | Yes | Lowercase vendor.name, such as acme.stale-work, at most 64 characters. The iskue. namespace is reserved. |
name | Yes | Display name, up to 200 characters. |
version | Yes | The plugin's own semantic version, such as 1.0.0. |
apiVersion | Yes | The plugin API versions the plugin was built for, as a range: "[0.3,1.0)". Iskue refuses a plugin whose range does not include its own API version. |
description | No | Up to 1,000 characters, shown to administrators. |
vendor | No | { name, url }. |
runtime | No | java, the only value for now. |
scopes | No | The access the plugin asks for (below). Administrators see them before enabling it. |
storage | No | migrations (default db/migration) and tables: each table the migrations create, as reference or ticket. |
scan | No | Your own Java packages, scanned for further Spring components. |
modules | Yes | At least one module: type, key (lowercase letters, digits and hyphens), plus class for a Java module or entry for a panel. Panels and workflow rules need a title (in English and French), shown in the interface; a TQL function needs a name, its camelCase name in TQL. |
Unknown fields are refused, so a typo fails at upload instead of being ignored. Iskue checks the descriptor and the JAR together and lists every problem at once.
Module types
| Module | You implement | What it does |
|---|---|---|
rest | A Spring @RestController | Endpoints under /api/plugins/{key}/…. Callers are always signed in, and the plugin acts as the caller. |
event-listener | event.TicketEventListener | Called once for each committed work-item change, on the server where it happened, on a bounded background pool — never on the request thread. The event carries the work item's key, its project, the kind of change (CREATED, UPDATED, STATUS_CHANGED, ASSIGNED, COMMENTED, LABEL_ADDED, … an open set) and who made it. Exceptions are logged and never undo the change. |
lifecycle | lifecycle.PluginLifecycle | onEnable() and onDisable() on each server. An exception in onEnable() marks the plugin failed on that server, and nothing of it is served there. |
ticket-panel | A JavaScript export | A side panel on the work item page (below). |
workflow-condition | workflow.WorkflowCondition | Whether a transition is offered: allows(ctx), with an optional reason(ctx) shown to the person. |
workflow-validator | workflow.WorkflowValidator | Checked when someone makes the move: validate(ctx) returns the message that stops it, or nothing. |
workflow-post-function | workflow.WorkflowPostFunction | Runs once the move has committed: execute(ctx). |
tql-function | search.TqlFunction | A TQL function such as key IN staleIn(OPS): ticketIds(args) returns the matching work items' ids. |
Every module is installed for the whole instance; project administrators choose which transitions use a plugin's workflow rules. Switching a plugin's features on per project is planned.
The API
Inject any of these into a module's constructor. Each one checks the plugin's scopes and the acting user's rights on every call.
| Service | Scope | What it does |
|---|---|---|
ticket.TicketApi | read:ticket, write:ticket, write:comment | get(key) → Optional<TicketView>, transitions(key), comments(key); create(NewTicket), update(key, TicketUpdate), transition(key, status); comment(key, body), internalNote(key, body) |
search.SearchApi | read:ticket | search(tql, page, pageSize) → SearchResult (at most 100 a page, with total and hasNext()), count(tql), validate(tql) — the results the Search page would show that user |
project.ProjectApi | read:project | get(key), visible() → ProjectView (id, key, name, description, lead, type, archived) |
user.UserApi | read:user | current(), get(id), byEmail(email) → UserView (id, display name, email, active, admin) |
permission.PermissionApi | None | isAdmin(), canBrowse(projectKey), canAdminister(projectKey), has(projectKey, EDIT_TICKET …) — about the acting user only |
settings.SettingsApi | None | get, set and all, for the whole instance or per project |
storage.StorageApi | storage | query(sql, args…) and update(sql, args…) on the plugin's own schema |
cluster.ClusterLock | None | tryRun(name, body), tryCall(name, supplier), run(name, timeout, body) |
PluginInfo | None | The plugin's own key, name and version |
TicketView carries id, key, projectKey, type, summary, status, statusCategory (TODO, IN_PROGRESS or DONE), priority, assignee and assigneeId, reporter, description (Markdown), labels, parentKey, dueDate, createdAt and updatedAt. Key your own rows by id: a work item's key changes when it moves to another project. NewTicket and TicketUpdate are built step by step, and a field you leave out keeps the project's default or its current value; clearing a field is explicit (withoutAssignee(), withoutDueDate()).
TicketView bug = tickets.create(NewTicket.of("OPS", "Bug", "Disk full on build agent")
.withPriority("HIGH")
.withDescription("Raised by the monitoring plugin."));
tickets.update(bug.key(), TicketUpdate.none().withDueDate(LocalDate.now().plusDays(2)));
tickets.transition(bug.key(), "In Progress"); // only a transition the workflow offers
tickets.internalNote(bug.key(), "Agent build-07, volume /var at 100%.");
SearchResult page = search.search("project = OPS AND assignee IS EMPTY ORDER BY created DESC", 0, 25);
long open = search.count("project = OPS AND statusCategory != Done");Who a plugin acts as
Every call runs as someone: the signed-in caller inside a rest endpoint, the person who made the change inside an event-listener or a post-function, the person moving the work item (or being offered the move) inside a condition or a validator, and the person searching inside a TQL function. It goes through the same membership, permission-scheme, restricted-work-item and workflow checks as Iskue's own REST API, so a plugin can do what its user could do — never more. A work item the user cannot see does not exist for the plugin either. A change made by the system has no user: UserApi.current() is empty and PermissionApi answers no.
A refusal throws PluginApiException with a reason and a message written for the person using the plugin. Let it escape from a rest endpoint and Iskue answers with the matching status, as its own API does:
| Reason | HTTP | When |
|---|---|---|
NOT_FOUND | 404 | The thing does not exist, or the user cannot see it — the two look the same on purpose. |
FORBIDDEN | 403 | The user can see it but may not do this. |
INVALID | 400 | The request is wrong: an unknown type or status, a value out of range, a TQL syntax error. |
CONFLICT | 409 | The current state does not allow it: an archived project, a transition the workflow does not offer. |
PluginScopeException | 403 | The plugin called something its descriptor did not ask for. Nothing is read or written. |
Scopes
| Scope | Administrators see | Unlocks |
|---|---|---|
read:ticket | Read the tickets its user can see | TicketApi reads, SearchApi |
write:ticket | Create, edit and move tickets as its user | create, update, transition |
write:comment | Comment on tickets as its user | comment, internalNote |
read:project | See the projects its user can browse | ProjectApi |
read:user | Look up people, including their email addresses | UserApi |
storage | Keep its own data | StorageApi and the plugin's tables |
Settings, permissions, the cluster lock and PluginInfo need no scope: they only reach the plugin's own state or the acting user's own rights.
Workflow rules
Conditions, validators and post-functions appear in the transition rule editor of every project as Plugin rule choices, under the title the descriptor gives them. A project administrator adds one to a transition and can give it settings, one name = value per line (at most 50); the rule reads them with ctx.config(name, fallback). Each rule receives a TransitionContext: the work item (before the move for a condition or a validator, after it for a post-function), the status it leaves and the one it enters, who is moving it, and the settings. See Transition rules for how rules combine with the built-in ones.
workflow-validator modulepackage com.acme.stale;
import com.iskue.plugin.api.workflow.TransitionContext;
import com.iskue.plugin.api.workflow.WorkflowValidator;
import java.util.Optional;
/** Stops a move until the work item is described; the minimum length is a setting of the rule. */
public class NeedsDescription implements WorkflowValidator {
@Override
public Optional<String> validate(TransitionContext ctx) {
int minLength = Integer.parseInt(ctx.config("minLength", "1"));
String description = ctx.ticket().description();
if (description == null || description.strip().length() < minLength) {
return Optional.of("Describe the work (at least " + minLength + " characters) before moving it to "
+ ctx.toStatus() + ".");
}
return Optional.empty();
}
}- Conditions are asked every time Iskue lists the moves open to someone — on every work item page and board — and again when the move runs. Keep them fast and free of side effects.
- Validators run inside the move's transaction, before the status changes. A message stops the move with HTTP 422, exactly like a built-in validator.
- Post-functions run after the move has committed, on the plugin pool and never on the request thread, so they can comment, update another work item or call out. A failure is logged and never undoes the move.
- Fail closed — a condition or validator that throws, or whose plugin is disabled, uninstalled or not running on that server, blocks the move and says why. A post-function whose plugin is not running is logged and skipped.
TQL functions
A tql-function module adds a function people can use as key IN staleIn(OPS) (or issue IN …), under the name the descriptor gives it, and TQL autocomplete offers it beside the built-in functions. It returns work item ids, at most 10,000 per call; Iskue then keeps only those the person searching may see, so a function can narrow a search but never widen it. It runs on every search that uses it, as the person searching — keep it fast. Throw PluginApiException with reason INVALID for bad arguments: the message is shown under the query. Function names are camelCase, 2 to 41 characters, and cannot reuse a built-in function's name.
tql-function modulepackage com.acme.stale;
import com.iskue.plugin.api.PluginApiException;
import com.iskue.plugin.api.PluginApiException.Reason;
import com.iskue.plugin.api.search.SearchApi;
import com.iskue.plugin.api.search.SearchResult;
import com.iskue.plugin.api.search.TqlFunction;
import com.iskue.plugin.api.settings.SettingsApi;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** key IN staleIn(OPS): the project's stale work items, by the threshold its administrators chose. */
public class StaleFunction implements TqlFunction {
private final SearchApi search;
private final SettingsApi settings;
public StaleFunction(SearchApi search, SettingsApi settings) {
this.search = search;
this.settings = settings;
}
@Override
public Set<String> ticketIds(List<String> args) {
if (args.size() != 1 || !args.get(0).matches("[A-Z][A-Z0-9_]*")) {
throw new PluginApiException(Reason.INVALID, "staleIn() takes one project key, like staleIn(OPS)");
}
String projectKey = args.get(0);
int days = settings.get(projectKey, "stale.days").map(Integer::parseInt).orElse(30);
String tql = "project = " + projectKey + " AND statusCategory != Done AND updated < -" + days + "d";
Set<String> ids = new HashSet<>();
for (int page = 0; ; page++) {
SearchResult result = search.search(tql, page, SearchApi.MAX_PAGE_SIZE);
for (var ticket : result.tickets()) {
if (ids.size() == MAX_RESULTS) {
return ids;
}
ids.add(ticket.id());
}
if (!result.hasNext()) {
return ids;
}
}
}
}Plugin data
- Own tables — a plugin with
storagegets its own database schema,plg_<key>(dots and hyphens become underscores), created and upgraded from the SQL migrations it ships underdb/migration.StorageApiruns each statement in its own transaction with that schema as the search path, so tables are named without a schema; parameters bind to?. - Placed for clusters — the descriptor says what each table is. A
referencetable is copied to every database node; atickettable is distributed byticket_id, next to the work items, on a distributed (Citus) database. The same plugin runs unchanged on a single server. - Settings —
SettingsApikeeps text values of up to 65,536 characters (store JSON for structure) for the instance and for each project, the same on every server; at most 1,000 per plugin, named witha-z 0-9 . _ -. It does not check who changes a setting: checkPermissionApi.isAdmin()orcanAdminister(projectKey)first, as the example does. - Uninstalling keeps a plugin's tables and settings, unless the administrator chooses to delete its data.
Your SQL runs on a distributed database in production. In ON CONFLICT … DO UPDATE SET, use only immutable functions: write updated_at = EXCLUDED.updated_at with now() in VALUES, never updated_at = now(). On a ticket table, every primary key and unique key must include ticket_id.
Built for clusters
Iskue runs several application servers in front of one database. A plugin installed once runs on every server: each server picks up an install, an upgrade or a switch-off within seconds, and the administration page shows each server's state. Events are delivered on the server where the change happened. For work that must not run twice — a sync with an outside system, a clean-up started on every server — take a ClusterLock: one holder across the whole cluster, released when the body returns or throws, and when its server dies. Keep the body short, since it holds a database connection.
A panel on the work item page
A ticket-panel module is a JavaScript module in the JAR's web/ folder. Iskue loads it when the panel appears and calls the export named in entry with the panel's element and a host object: apiVersion, pluginKey, moduleKey, ticketKey, locale (en or fr) and request(path, init), which calls the plugin's own REST endpoints as the signed-in user and resolves to the parsed JSON. Return a function to clean up when the panel goes away. Give the panel's title in English and French in the descriptor.
// web/stale-panel.js, declared as entry: web/stale-panel.js#StalePanel
export function StalePanel(container, host) {
const text = { en: "Stale work in this project", fr: "Travail en sommeil dans ce projet" };
const heading = document.createElement("p");
heading.textContent = text[host.locale] ?? text.en;
const list = document.createElement("ul");
container.append(heading, list);
const projectKey = host.ticketKey.slice(0, host.ticketKey.lastIndexOf("-"));
host.request(`/projects/${projectKey}/stale`).then((keys) => {
for (const key of keys) {
const item = document.createElement("li");
item.textContent = key; // textContent keeps text out of the HTML parser
list.append(item);
}
});
return () => container.replaceChildren(); // called when the panel goes away
}In the alpha a panel runs in the page itself, not in a sandbox — one reason uploads stay off on hosted instances until signed packages and a Content-Security-Policy for plugin code ship.
Build and package
- Compile for Java 21 against
com.iskue.plugin.api, and putiskue-plugin.yamlat the root of the JAR. - Iskue provides the API and these shared libraries, so mark them
provided— it refuses a JAR that bundles classes in their packages:org.slf4j,com.fasterxml.jackson.annotation,jakarta.annotation,jakarta.validation,org.springframework.web.bind.annotation,org.springframework.http,org.springframework.stereotype,org.springframework.beans.factory.annotationandorg.springframework.context.annotation. Shade any other library you use into your JAR. - Compile with
-parameters, or name every@PathVariableand@RequestParam, as the example does. - A JAR holds at most 10,000 entries and 64 MB uncompressed. Every module's
classorentrymust be in it, and a plugin with storage must ship at least one migration.
There is no project template or iskue:run yet. Design partners get the API JAR and a sample plugin that uses every module type — a work item panel backed by its own REST endpoint and tables, an event listener and a lifecycle — with its Maven build.
Install and run
Global administrators manage Java plugins in Administration → Plugins → Java plugins (alpha): upload a JAR, review the access it asks for, enable it (a new plugin arrives disabled), see whether each server runs it, disable it, and uninstall it with or without its data. Java plugins are an Enterprise edition feature; an instance with no licence installed is unrestricted.
| Variable | Default | Purpose |
|---|---|---|
| APP_PLUGINS_UPLOAD_ENABLED | false | Allows uploading plugin JARs. Packages are not signed yet, so turn it on only where you trust every administrator and every JAR they upload. |
| APP_PLUGINS_SAFE_MODE | false | Starts Iskue without running any Java plugin — the way back from a plugin that misbehaves. |
| APP_PLUGINS_CACHE_DIR | iskue-plugins in the system temporary folder | Where each server keeps the plugin JARs it runs. |
Versions
ApiVersion.CURRENTis0.3.0. API 0.1 brought REST endpoints, work-item events, the lifecycle, the work item panel and plugin storage; 0.2 added work-item writes and comments, TQL search, projects, people, permissions, settings and the cluster lock; 0.3 added workflow conditions, validators and post-functions, and TQL functions.- Iskue refuses to install a plugin whose
apiVersionrange does not include its own API version. During 0.x a minor version may change the API; from 1.0, changes within a major version stay backwards compatible. - Types are marked
@PublicApi(call it) or@PublicSpi(implement it); the whole package is@ExperimentalApiuntil 1.0.
Planned
Planned — not available yet. Everything from here on describes the plugin platform Iskue is building, so partners and customers can plan ahead. Names and details may change.
More module types
Each module plugs into one named extension point. The ones Data Center apps use most come first; the rest follow in the partner beta.
| Module | What it adds | Jira Data Center equivalent |
|---|---|---|
ticket-tab, ticket-action | An activity tab or a menu action on the work item page | Issue tab panel, issue operation |
web-item, web-section | Declarative links and menu entries, no code | Web item, web section |
admin-page, project-settings-page, project-page | Full pages in Administration, Project settings or the project | Admin page, project tab panel |
custom-field-type | A field type end to end: storage, validation, search in TQL, display and editing, export | Custom field type + searcher |
automation-trigger, -condition, -action | New building blocks in the automation rule builder | Automation extensions |
scheduled-job | Background work on a schedule, run once per cluster | Scheduled job |
project-permission, global-permission | Permissions granted in permission schemes like built-in ones | Project / global permission |
dashboard-gadget, doc-block, report | Dashboard gadgets, blocks for Docs pages, reports | Gadget, Confluence macro, report |
board-card-badge, sheet-column | Badges on board cards, computed columns in sheets and structures | — |
entity-property-index, webhook-event | Searchable plugin data on work items; plugin events for outbound webhooks | Entity property index |
A fuller API
- A published artefact —
com.iskue:iskue-plugin-api, semantically versioned, so a plugin builds anywhere. - Groups, workflows and fields — through services that apply the same rules as the user interface.
- Events — a typed, versioned event vocabulary for comments, SLAs, sprints, releases, projects and users, independent of Iskue's internal events.
- Platform services — translations, transactions, notifications and an HTTP client limited to the hosts the descriptor declares.
- Cluster services — a cluster-wide cache and a job scheduler, beside today's lock.
- A service user — work a plugin does on its own, such as a scheduled job, runs as the plugin's service user, and its changes show "via <plugin>" in the history.
User interface
Today a panel is plain JavaScript (above). The planned @iskue/plugin-ui package adds React components loaded on demand into named slots of the Iskue interface, rendered with the same design tokens in light and dark mode, with typed slot registration, an authenticated client for Iskue's and the plugin's own REST API, navigation, dialogs, notifications and base components. Each slot will be isolated: a failing plugin shows a message in its own area and never breaks the page.
import { definePlugin, Panel, useTicket, useApi } from "@iskue/plugin-ui";
export const NotesPanel = definePlugin.ticketPanel(() => {
const ticket = useTicket();
const { data } = useApi(`/api/plugins/acme.release-notes/notes/${ticket.key}`);
return <Panel title="Release notes">{data?.text ?? "No notes yet."}</Panel>;
});More plugin data
- Entity properties — small JSON values attached to work items, projects or users, searchable in TQL when the descriptor indexes them.
Security and trust
A Java plugin runs inside Iskue with the application's privileges — exactly as a Jira Data Center app does — so installing one is a decision to trust its vendor. Today administrators see the access a plugin asks for before enabling it, every call is checked against those scopes and the acting user's rights, and safe mode starts Iskue without any plugin. Planned on top:
- Signed packages — plugins are signed by their vendor and by the marketplace; uploading unsigned packages stays off unless an administrator turns it on.
- Upgrades that ask — an upgrade that asks for more access waits for approval.
- Checked on upload — packages are scanned for dangerous code before they can be enabled.
- Contained at run time — time limits, separate thread pools and circuit breakers switch off a misbehaving module automatically.
- Isolated user interface — a Content-Security-Policy and isolated slots for plugin code in the browser.
- Audited — every install, upgrade, permission grant and configuration change is recorded, and changes a plugin makes show "via <plugin>" in the history.
For logic you do not want to trust with in-process access, use a script plugin (sandboxed) or, later, a remote app (runs outside Iskue).
Administration
Global administrators will install, upgrade and remove plugins in Administration → Apps, with per-module switches, health metrics and errors. Project administrators then switch a plugin's project features on or off and configure them for their own project in Project settings → Apps — configuration stays with the people who run the project, as everywhere else in Iskue. Plugins remain an Enterprise edition feature.
Developer tooling
- Project template — a Maven archetype with the backend, a frontend, translations, tests and a CI workflow.
iskue:run— starts a local Iskue in Docker with your plugin installed, reinstalls it when the code changes and reloads its UI;iskue:debugattaches a debugger.- Test harness — a JUnit 5 extension that boots a real Iskue (optionally a two-server cluster) with test data, plus helpers for UI tests.
- Packaging —
iskue:validate,iskue:package,iskue:signand an API-compatibility check; a command-line tool installs a build on any instance with an access token. - Samples — one sample per module family, and a worked port of an open-source Jira Data Center app.
mvn archetype:generate -DarchetypeGroupId=com.iskue -DarchetypeArtifactId=iskue-plugin-archetype
cd release-notes
mvn iskue:run # http://localhost:8088 with the plugin installedCompatibility from 1.0
- Iskue releases get a product version; plugins declare the API versions they support, and Iskue refuses to enable an incompatible plugin with a clear message.
- Within a major API version, changes are backwards compatible — enforced by an automated check on every Iskue build, not by promise.
- Deprecated API stays available for at least two minor releases and six months, and every release publishes an API changelog.
Marketplace and partners
The marketplace that exists today for script plugins will list Java plugins too, with screenshots, categories, changelogs and compatibility. Vendors will publish through a vendor portal, where each version is checked automatically (descriptor, signature, compatibility, code scan) before review. Plugins can be free or paid, with trials: plugin licences are issued by the marketplace and checked by the plugin through the API. Plugins that pass an automated test on a multi-server cluster earn a Cluster Ready badge — the counterpart of Data Center approved apps. A partner programme will set out listing guidelines, revenue share and support expectations.
Coming from Jira Data Center
| Jira Data Center | Iskue |
|---|---|
atlassian-plugin.xml | iskue-plugin.yaml |
| Plugin Framework (OSGi bundles, exported packages) | An isolated class loader per plugin; only com.iskue.plugin.api and a few shared libraries are visible |
component, component-import | Module classes receive the API by constructor injection; scan picks up your own components |
Java API (@PublicApi, @PublicSpi) | com.iskue.plugin.api (@PublicApi, @PublicSpi, @ExperimentalApi); a published iskue-plugin-api artefact is planned |
| Shared Access Layer (settings, users, i18n, transactions) | SettingsApi, UserApi and PermissionApi today; I18nApi and TransactionApi planned |
| Active Objects | A per-plugin schema with your own migrations, plus StorageApi |
| Entity properties | Entity properties, TQL-indexable (planned) |
| Web items, sections, panels, web resources | The work item panel today; web items, more slots and the UI SDK planned |
| Workflow conditions, validators and post-functions | workflow-condition, workflow-validator, workflow-post-function, with settings entered on the transition |
| JQL function | tql-function |
| Cluster lock, cluster cache, scheduler | ClusterLock today; ClusterCache and JobScheduler planned |
| Universal Plugin Manager | Administration → Plugins → Java plugins (alpha) today; Administration → Apps and Project settings → Apps planned |
Atlassian SDK (atlas-run, atlas-package) | Maven archetype and plugin (iskue:run, iskue:package), planned |
| Marketplace and Data Center approved apps | The Iskue marketplace and the Cluster Ready badge, planned for Java plugins |
A migration guide with side-by-side code for each row will accompany the partner beta.
Rollout
| Stage | What it brings |
|---|---|
| Internal alpha — now | The framework and API 0.1–0.3: REST endpoints, work-item events, the work item panel, plugin storage, work items, search, projects, people, permissions, settings, the cluster lock, workflow rules and TQL functions. Still to come in this stage: the project template and iskue:run |
| Partner beta | All module types above, signed packages, the published API artefact, administration, security guardrails, the full developer guide — validated with design partners; the API is frozen as version 1.0 |
| General availability | The marketplace for Java plugins, the vendor portal, plugin licensing, the Cluster Ready programme, script plugins v2 |
| Later | Remote apps that run outside Iskue |
Building Jira Data Center apps today? We are looking for design partners to try the alpha and shape the API before 1.0. Get in touch.
Feature overview: Made to fit your toolchain