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

KindRunsWritten inForStatus
Java pluginsInside Iskue, like Jira Data Center appsJava 21 (plus JavaScript for the UI)Partners porting Data Center apps, internal platform teamsAlpha — API 0.3, described below
Script pluginsIn a locked-down JavaScript sandbox inside IskueJavaScriptAdministrators and customers automating their instanceAvailable today (Plugins); will gain the same descriptor, read access, storage and allow-listed HTTP
Remote appsOn the vendor's own serversAny languageSaaS vendorsPlanned, 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.yaml
key: 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 }
A 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";
    }
}
An event-listener module
package 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

FieldRequiredMeaning
keyYesLowercase vendor.name, such as acme.stale-work, at most 64 characters. The iskue. namespace is reserved.
nameYesDisplay name, up to 200 characters.
versionYesThe plugin's own semantic version, such as 1.0.0.
apiVersionYesThe 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.
descriptionNoUp to 1,000 characters, shown to administrators.
vendorNo{ name, url }.
runtimeNojava, the only value for now.
scopesNoThe access the plugin asks for (below). Administrators see them before enabling it.
storageNomigrations (default db/migration) and tables: each table the migrations create, as reference or ticket.
scanNoYour own Java packages, scanned for further Spring components.
modulesYesAt 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

ModuleYou implementWhat it does
restA Spring @RestControllerEndpoints under /api/plugins/{key}/…. Callers are always signed in, and the plugin acts as the caller.
event-listenerevent.TicketEventListenerCalled 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.
lifecyclelifecycle.PluginLifecycleonEnable() and onDisable() on each server. An exception in onEnable() marks the plugin failed on that server, and nothing of it is served there.
ticket-panelA JavaScript exportA side panel on the work item page (below).
workflow-conditionworkflow.WorkflowConditionWhether a transition is offered: allows(ctx), with an optional reason(ctx) shown to the person.
workflow-validatorworkflow.WorkflowValidatorChecked when someone makes the move: validate(ctx) returns the message that stops it, or nothing.
workflow-post-functionworkflow.WorkflowPostFunctionRuns once the move has committed: execute(ctx).
tql-functionsearch.TqlFunctionA 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.

ServiceScopeWhat it does
ticket.TicketApiread:ticket, write:ticket, write:commentget(key) → Optional<TicketView>, transitions(key), comments(key); create(NewTicket), update(key, TicketUpdate), transition(key, status); comment(key, body), internalNote(key, body)
search.SearchApiread:ticketsearch(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.ProjectApiread:projectget(key), visible() → ProjectView (id, key, name, description, lead, type, archived)
user.UserApiread:usercurrent(), get(id), byEmail(email) → UserView (id, display name, email, active, admin)
permission.PermissionApiNoneisAdmin(), canBrowse(projectKey), canAdminister(projectKey), has(projectKey, EDIT_TICKET …) — about the acting user only
settings.SettingsApiNoneget, set and all, for the whole instance or per project
storage.StorageApistoragequery(sql, args…) and update(sql, args…) on the plugin's own schema
cluster.ClusterLockNonetryRun(name, body), tryCall(name, supplier), run(name, timeout, body)
PluginInfoNoneThe 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()).

Work items and search, as the acting user
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:

ReasonHTTPWhen
NOT_FOUND404The thing does not exist, or the user cannot see it — the two look the same on purpose.
FORBIDDEN403The user can see it but may not do this.
INVALID400The request is wrong: an unknown type or status, a value out of range, a TQL syntax error.
CONFLICT409The current state does not allow it: an archived project, a transition the workflow does not offer.
PluginScopeException403The plugin called something its descriptor did not ask for. Nothing is read or written.

Scopes

ScopeAdministrators seeUnlocks
read:ticketRead the tickets its user can seeTicketApi reads, SearchApi
write:ticketCreate, edit and move tickets as its usercreate, update, transition
write:commentComment on tickets as its usercomment, internalNote
read:projectSee the projects its user can browseProjectApi
read:userLook up people, including their email addressesUserApi
storageKeep its own dataStorageApi 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.

A workflow-validator module
package 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.

A tql-function module
package 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 storage gets its own database schema, plg_<key> (dots and hyphens become underscores), created and upgraded from the SQL migrations it ships under db/migration. StorageApi runs 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 reference table is copied to every database node; a ticket table is distributed by ticket_id, next to the work items, on a distributed (Citus) database. The same plugin runs unchanged on a single server.
  • Settings — SettingsApi keeps 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 with a-z 0-9 . _ -. It does not check who changes a setting: check PermissionApi.isAdmin() or canAdminister(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.

A panel needs no framework
// 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 put iskue-plugin.yaml at 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.annotation and org.springframework.context.annotation. Shade any other library you use into your JAR.
  • Compile with -parameters, or name every @PathVariable and @RequestParam, as the example does.
  • A JAR holds at most 10,000 entries and 64 MB uncompressed. Every module's class or entry must 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.

VariableDefaultPurpose
APP_PLUGINS_UPLOAD_ENABLEDfalseAllows 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_MODEfalseStarts Iskue without running any Java plugin — the way back from a plugin that misbehaves.
APP_PLUGINS_CACHE_DIRiskue-plugins in the system temporary folderWhere each server keeps the plugin JARs it runs.

Versions

  • ApiVersion.CURRENT is 0.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 apiVersion range 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 @ExperimentalApi until 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.

ModuleWhat it addsJira Data Center equivalent
ticket-tab, ticket-actionAn activity tab or a menu action on the work item pageIssue tab panel, issue operation
web-item, web-sectionDeclarative links and menu entries, no codeWeb item, web section
admin-page, project-settings-page, project-pageFull pages in Administration, Project settings or the projectAdmin page, project tab panel
custom-field-typeA field type end to end: storage, validation, search in TQL, display and editing, exportCustom field type + searcher
automation-trigger, -condition, -actionNew building blocks in the automation rule builderAutomation extensions
scheduled-jobBackground work on a schedule, run once per clusterScheduled job
project-permission, global-permissionPermissions granted in permission schemes like built-in onesProject / global permission
dashboard-gadget, doc-block, reportDashboard gadgets, blocks for Docs pages, reportsGadget, Confluence macro, report
board-card-badge, sheet-columnBadges on board cards, computed columns in sheets and structures—
entity-property-index, webhook-eventSearchable plugin data on work items; plugin events for outbound webhooksEntity 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.

A work item side panel (draft SDK)
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:debug attaches 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:sign and 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.
Planned workflow
mvn archetype:generate -DarchetypeGroupId=com.iskue -DarchetypeArtifactId=iskue-plugin-archetype
cd release-notes
mvn iskue:run          # http://localhost:8088 with the plugin installed

Compatibility 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 CenterIskue
atlassian-plugin.xmliskue-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-importModule 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 ObjectsA per-plugin schema with your own migrations, plus StorageApi
Entity propertiesEntity properties, TQL-indexable (planned)
Web items, sections, panels, web resourcesThe work item panel today; web items, more slots and the UI SDK planned
Workflow conditions, validators and post-functionsworkflow-condition, workflow-validator, workflow-post-function, with settings entered on the transition
JQL functiontql-function
Cluster lock, cluster cache, schedulerClusterLock today; ClusterCache and JobScheduler planned
Universal Plugin ManagerAdministration → 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 appsThe 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

StageWhat it brings
Internal alpha — nowThe 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 betaAll 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 availabilityThe marketplace for Java plugins, the vendor portal, plugin licensing, the Cluster Ready programme, script plugins v2
LaterRemote 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.