Plugin Development

Write a plugin

Plugins add new tools to AI WorkDeck so the AI can call your code from a conversation. This guide starts with a minimal working example and ends with submitting for review.

Кто может публиковать плагины

Бесплатные плагины с открытым исходным кодом может отправить любой авторизованный аккаунт — верификация не требуется.

Установка цены на плагин или Pack, а также публикация закрытого кода требуют верифицированного аккаунта разработчика (личность + способ выплат, проверяется вручную). Заявку можно подать в

Центре разработчиков

SDK 1.6 views, connections, MCP, scoped settings, $schema/default compatibility fixes and host services require the new host. This implementation has not been released; installed clients do not gain support automatically. The simulator uses synthetic data and is not host compatibility evidence.

First, decide: do you need a plugin or a Skill

The two are easy to confuse, and picking the wrong one wastes a lot of work. The difference fits in one sentence: if it takes code, write a plugin; if explaining it clearly is enough, write a Skill.

ScenarioUse
Call an external system’s API to fetch dataPlugin
Parse a special file formatPlugin
Have the AI write review comments in your firm’s formatSkill
Define the steps and deliverable structure for a type of matterSkill

A Skill is plain text (a prompt plus trigger words): it goes live on submission with no review, and is much faster to write. If a Skill covers your need, go write a Skill and skip the rest of this page.

Run your first plugin in five minutes

A plugin is a Java project compiled into a JAR, plus a manifest.json, zipped up and submitted. You need JDK 21 and Maven.

Download the template project above — it is itself a complete working example. Only two files matter. The first is the tool class:

package com.example.myplugin;

import dev.langchain4j.agent.tool.Tool;

/**
 * Plugin tool class.
 *
 * Three hard rules — break any one of them and the tool will not be visible to the AI:
 * 1. Must have a no-arg constructor (instantiated via reflection by the host);
 * 2. Annotate tool methods with @Tool; the method name is the tool name and must match manifest.json tools[].name;
 * 3. Use String parameters and return values for simplicity (serialize complex structures as JSON strings).
 *
 * The @Tool description is read by the AI and determines when it invokes this tool.
 * State clearly when to call it rather than only how it is implemented.
 */
public class MyTools {

    @Tool("Count characters and CJK characters in a text passage and return a readable summary. Call when the user asks for character or word counts.")
    public String countChinese(String text) {
        if (text == null || text.isBlank()) {
            return "Empty input: 0 characters";
        }
        long cjk = text.codePoints()
                .filter(cp -> Character.UnicodeScript.of(cp) == Character.UnicodeScript.HAN)
                .count();
        return String.format("Total %d chars, including %d CJK chars", text.length(), cjk);
    }
}

The description in @Tool is written for the AI — it directly decides whether the AI calls your tool at the right moment. Writing "when to use it" is far more useful than "what this method does": the former is what the AI has to judge.

The second is manifest.json, which describes what the plugin is:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "One sentence explaining what this plugin does for users, shown on the marketplace card.",
  "author": "Your Name or Team",
  "homepage": "https://example.com",
  "permissions": [],
  "tools": [
    {
      "name": "countChinese",
      "description": "Count characters in text",
      "permissions": []
    }
  ],
  "backendJars": ["my-plugin-1.0.0.jar"]
}

Tool names must match on both sides: tools[].name must equal the Java method name. If they differ, the tool fails to register and the AI cannot see it.

Then package it:

mvn package
mkdir -p dist && cp target/my-plugin-1.0.0.jar manifest.json dist/
cd dist && zip -r ../my-plugin-1.0.0.zip . && cd ..

Note the zip contains the files themselves — don’t nest an extra directory. Unzipping should reveal manifest.json directly, not a folder.

Try it on your own machine before submitting

No need to wait for review. Copy the entire dist/ directory into your local plugin folder and restart AI WorkDeck:

# macOS / Linux
~/.aiworkdeck/plugins/my-plugin/

Your plugin should appear under Plugin Marketplace → Installed. Enable it, then ask something in a conversation that should use your tool and see whether the AI calls it. If it doesn’t, the @Tool description most likely fails to explain when to use it.

What each manifest.json field means

FieldDescription
idGlobally unique; lowercase letters, digits and hyphens. It can never change once published — it is how upgrades identify "the same plugin".
versionSemantic version. Every submission must be higher than the previous one, or it is rejected.
name / descriptionShown on the marketplace card. Describe what it does for the user, not the implementation.
author / homepageAuthor and project homepage. Optional but recommended — users judge trust by them.
permissionsThe capabilities this plugin uses; see the next section.
toolsTool list. name must equal the Java method name; write the description so its purpose is clear.
backendJarsJAR file names relative to the package root. ../ paths outside the package are not allowed.

permissions: declare honestly — review cross-checks

Four optional values; declare what you use:

  • file_readRead project files
  • file_writeCreate, modify or delete files
  • networkAccess the external network
  • editorOperate the document editor

This is not a sandbox

Plugins run in the same process as the host app, so undeclared behavior cannot be blocked technically — a tool that declares no permissions can still read files. The declaration is not a runtime restriction but review evidence: we cross-check it against the JAR’s static scan. Declaring no network while referencing network APIs — or the reverse — gets the submission rejected.

What review looks at

Every version is reviewed by a human, usually within one to two business days. An automated scan runs first, and its report sits next to your permissions declaration on the reviewer’s desk.

These are rejected outright:

  • Declared permissions don’t match the APIs actually called
  • Custom TrustManager or any other way of bypassing certificate validation
  • Hardcoded IP addresses, or data sent out over plaintext HTTP
  • Reflection into the host’s internal objects (database connections, config services, etc.)
  • Obfuscated or packed code, or anything that hides what the code does
  • Version number not higher than the previous one

Once approved, the platform signs the whole package with its private key and clients verify the signature on install. After that, nobody — including us — can alter the package contents without re-signing.

If a problem surfaces after release, we revoke that version. Clients pick up the revocation list, disable it automatically and notify the user.

A promise to users — and a constraint on you

Our users are lawyers, and their machines hold clients' confidential material. A plugin gets the same access as the host app — far beyond what it needs for itself.

Поэтому проверка ведётся с более строгой стороны, а отклонения сопровождаются указанием причин. Если вашему плагину действительно требуется чувствительная функция, объясните причину в комментариях к отправке — это значительно ускорит проверку.

Веб-плагины: создание интерфейса на HTML/JS

Если вам нужен интерфейс — форма, инструмент поиска, панель мониторинга — Java не требуется. Поместите web/ каталог в пакет (обычный статический HTML/JS/CSS, точка входа web/index.html) и укажите на него в манифесте frontendEntry. Пакет может вообще не содержать JAR.

Здесь разрешения реальные. Настольное приложение загружает веб-плагин в изолированный iframe, намеренно не same-origin: скрипты плагина никогда не видят сессию хоста, каждая функция проходит через мост postMessage, и хост проверяет каждый вызов на соответствие разрешениям манифеста. В отличие от JAR-плагина, это объявление является границей принудительного контроля, а не самоописанием.

web/awd-plugin-sdk.js в шаблоне оборачивает этот мост. До завершения рукопожатия ничего не работает:

МетодПараметрыРезультатРазрешение
settings.open / settings.update{} / {values}{opened} / {}user / project settings; installation settings stay in trusted host UI
connections.list / connect / disconnect / request{id, operation?, params?}connection state / service JSONnetwork; host-held credentials and explicit developer trust
mcp.servers / mcp.call{} / {serverId, name, arguments}{servers} / MCP CallToolResultnetwork + project access; public HTTPS Streamable HTTP
billing.status / billing.checkout{connectionId?}{provider, active, plan?} / {opened}host purchase entitlement or developer subscription; no automatic charge
services.request / services.cancel{service, operation, params, requestId} / {requestId}operation resultexplicit service catalog; per-operation permission checks
context.get{}{ pluginId, projectId, language, theme, themeTokens }-
files.list{}{ files: [{ path, name, size }] }file_read
im.downloadAttachment{ conversationId, attachmentId }{ started, name }im (server-side attachment ACL check; no project write permission required)
notifications.publish{ eventKey, title, body?, actionState?, target? }notificationnotifications (current account inbox; target opens only this plugin)
notifications.resolve{ eventKey, state }notificationnotifications (updates only this plugin’s notifications; marking read never completes business actions)
im.request{ action, params }objectim (host 0.61.0+; fixed messaging actions, project archiving also requires file_write)
chat.addFile{ path }{}file_read (host 0.61.0+; attaches material without auto-sending)
files.prepareDrag{ path }{ fileId, dragToken, dragType, expiresAt, ... }file_read (host 0.61.0+; short-lived single-use drag ticket)
files.endDrag{}{}file_read (host 0.61.0+)
files.read{ path }{ path, content, truncated }file_read
ui.toast{ message }{}-
storage.get{ key }{ key, value }-
storage.set{ key, value }{}-
evidence.link{ anchor: { selection: true } | { quote }, docPath?, targets: [{ path, locator?, relation?, method?, note? }] }{ linkKey, targetIds }editor
evidence.list{ docPath?, path?, sectionPath?, status? }{ links: [{ linkKey, docPath, anchorText, sectionPath, status, targets }] }file_read
evidence.locate{ linkKey, targetId? }{}editor
tools.invoke{ name, args? }{ output }- (tool must be declared in this plugin’s manifest)
chat.send{ prompt }{}- (up to 4,000 characters)
ui.openFile{ path }{}file_read
doc.exec{ action, params? }{ result }editor (host 0.27.4+; action restricted to safe doc_/sheet_/slide_ subset)
doc.active{}{ fileId, kind }editor (host 0.27.4+)
events.subscribe{ events: ["files.changed" | "selection.changed" | "project.switched"] }{ subscribed }per event (host 0.27.4+)
events.unsubscribe{ events }{ subscribed }-
ai.request{ prompt, system?, purpose? }{ text, modelId }ai (host 0.27.4+; 16,000 chars, 10 calls/min, billed via user Credits)
settings.get{ key }{ key, value }- (host 0.28+; reads non-secret keys declared in top-level manifest settings)
<script src="awd-plugin-sdk.js"></script>
<script>
  const ctx = await awd.ready();                   // { pluginId, projectId, language, theme, themeTokens }
  const files = await awd.files.list();            // requires file_read
  const doc = await awd.files.read(files[0].path); // { path, content, truncated }
  await awd.ui.toast('Completed');
  await awd.storage.set('draft', { title: 'x' });  // plugin-scoped KV, capped at 64 KB
</script>

Ошибки приходят как отклонённые промисы с err.code: permission_denied, когда манифест не объявляет требуемое для вызова, unknown_method, когда хост не распознаёт метод. files.read ограничивает текст 5 МБ (truncated становится true), а storage ограничивает каждый плагин 64 КБ общего объёма.

Канал темы (v2.6)

Контекст рукопожатия теперь несёт theme ('light'|'dark') и themeTokens (таблицу CSS-переменных --awd-*); хост также отправляет { type: 'theme', theme, tokens } при каждом последующем переключении. SDK применяет это автоматически — устанавливает data-theme, переключает класс awd-theme-light/awd-theme-dark и записывает каждый токен как CSS-пользовательское свойство — поэтому CSS плагина может просто использовать var(--awd-surface) без единой строки JS. Для поведения на скриптах используйте awd.theme.get() для чтения текущего значения и awd.theme.onChange(cb) для подписки. В симуляторе хоста есть кнопка переключения светлой/тёмной темы в заголовке, так что вы можете тестировать обе темы без установки настольного приложения.

Шаблон также включает симулятор хоста, dev/host-simulator.html: он имитирует настольный хост, выполняет рукопожатие, обслуживает каждый метод фейковыми данными и логирует каждое сообщение моста. Запустите локальный статический сервер — и вы можете вести разработку в браузере без установки AI WorkDeck.

python3 -m http.server 8000
# -> http://localhost:8000/dev/host-simulator.html

Открытие напрямую через file:// обычно показывает пустой фрейм — браузеры отказываются загружать изолированные дочерние документы из file://. Сервер также не должен переписывать URL: npx serve по умолчанию включает clean URLs, перенаправляя /web/index.html на /web, из-за чего относительная ссылка на SDK выдаёт 404.

SDK 1.6 platform APIs (new host preview)

The Web download includes a standalone Node.js 20+ CLI. Keep scripts/ and sdk/plugin-sdk/ in their relative locations. Test UI in the simulator, then permissions, project switching, disabling, cancellation and reconnection in a compatible isolated host. Example domains are placeholders, not working services.

Declare sidebar/workspace views with project/global scope; one view per placement and an existing web/ entry. Sidebar docks are left/right/bottom. Settings default to installation scope; user/project settings may be changed by the plugin. Installation settings remain in trusted host UI. Secrets never return to the iframe; use connections for per-user credentials.

Connections declare a fixed public HTTPS origin and fixed GET/POST operations. Authentication is none, Bearer or public-client OAuth2 PKCE. connect opens trusted host UI; tokens stay in the host. Never pass arbitrary URLs, authorization headers or host setting keys. BYO services and shared host AI/speech channels are separate choices with their own permissions and usage constraints.

OAuth uses Authorization Code with S256 PKCE. A local host uses its actual port at http://127.0.0.1:<port>/api/plugins/oauth/callback; server deployments must set ai.plugins.oauth-redirect-uri to a trusted HTTPS callback and register it with the provider. Expired credentials without refresh or credentials rejected with 401/403 return needsReconnect and require host-side login again.

MCP currently supports public HTTPS Streamable HTTP only: no stdio, local processes, private addresses or redirects. Declare network, every server and tool; remote names and schemas must match. Input roots are objects. Supported schema keywords are type/title/description, object properties/required/boolean additionalProperties, array items and string enum. Types include boolean/integer/number. No $ref, oneOf, nullable or other keywords. Limits: depth 8, 64 properties per object and 64 tools per server. connectionId references a same-origin connection in this plugin. Legacy credentialKey only references a declared installation secret.

Development installs require explicit administrator service trust in the host settings page. Trust is bound to the full manifest SHA256 and expires when it changes. Packages must not contain .awd-dev, .awd-service-trust or .awd-bundled markers. Trust does not unlock development JARs or local processes.

speech, tasks and clipboard permissions grant only catalogued services.request operations; this is not an arbitrary backend proxy. services.cancel cancels this plugin’s requests. Handle unsupported hosts, missing permissions and failures explicitly; do not silently switch to external or paid services.

Platform billing currently covers plugin purchase entitlement only; submitted prices are one-time purchases. There is no platform per-call collection or revenue-sharing API. Developer-owned subscriptions may declare connections.billing status and checkout. An opened checkout is not a successful payment.

Host billing.status refreshes the current account entitlement and returns refreshed; false means the refresh did not succeed and active may come from a still-valid cache, not a fresh payment confirmation. Checkout waits for the browser-opening result and rejects failures. Test actual host login, token expiry, subscription status, MCP errors, account switching and shared/BYOK routing; a synthetic provider verifies host integration, not live OAuth or payment certification.

Complete the free developer eligibility review and generate a submission token in the developer center. Supply it through the local AWD_PLUGIN_TOKEN environment variable. Never put it in source, Agent prompts or packages. Valid submissions remain pending human review and signing; passing local validation is not marketplace approval.

node scripts/plugin-dev.mjs validate .
node scripts/plugin-dev.mjs package . --out ../my-plugin.zip
# Start the synthetic UI simulator, then open /dev/host-simulator.html
python3 -m http.server 8000
# Explicit development installation: select an isolated imported project/folder first.
# Replace PORT with the isolated host port. AWD_SESSION_ID is supplied locally if required.
# Do not paste credentials into source or an Agent prompt.
node scripts/plugin-dev.mjs install --endpoint http://127.0.0.1:PORT --project-id PROJECT_ID --folder-id FOLDER_ID
# Explicit marketplace submission only; provide AWD_PLUGIN_TOKEN locally.
node scripts/plugin-dev.mjs submit . --endpoint https://www.aiworkdeck.com/api/plugins --price-cents 0
node scripts/plugin-dev.mjs submissions --endpoint https://www.aiworkdeck.com/api/plugins
{
  "id": "my-service-plugin",
  "name": "Service panel",
  "version": "1.0.0",
  "description": "SDK 1.6 host preview example",
  "author": "Example developer",
  "frontendEntry": "web/index.html",
  "permissions": [
    "network",
    "ai",
    "speech",
    "tasks",
    "clipboard"
  ],
  "settings": [
    {
      "key": "language",
      "label": "Language",
      "type": "string",
      "scope": "user",
      "default": "zh-CN"
    },
    {
      "key": "summaryStyle",
      "label": "Summary style",
      "type": "string",
      "scope": "project",
      "default": "brief"
    }
  ],
  "contributes": {
    "views": [
      {
        "id": "service-panel",
        "name": "Service panel",
        "entry": "web/index.html",
        "placement": "sidebar",
        "scope": "project",
        "defaultDock": "left",
        "allowedDocks": [
          "left",
          "right",
          "bottom"
        ]
      }
    ],
    "connections": [
      {
        "id": "service",
        "label": "My service",
        "baseUrl": "https://api.example.com",
        "auth": {
          "type": "bearer"
        },
        "operations": {
          "summarize": {
            "method": "POST",
            "path": "/v1/summarize"
          },
          "status": {
            "method": "GET",
            "path": "/v1/subscription"
          }
        },
        "billing": {
          "statusOperation": "status",
          "checkoutUrl": "https://example.com/subscribe"
        }
      }
    ],
    "mcpServers": [
      {
        "id": "remote",
        "transport": "streamable-http",
        "url": "https://api.example.com/mcp",
        "connectionId": "service",
        "tools": [
          {
            "name": "summarize",
            "description": "Summarize supplied text",
            "inputSchema": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              },
              "required": [
                "text"
              ],
              "additionalProperties": false
            }
          }
        ]
      }
    ]
  }
}

Complete task to give your Agent

Implement this requirement using the downloaded AI WorkDeck Web plugin template: [feature, inputs, outputs, acceptance criteria].
Read README.md, sdk/plugin-sdk/awd-plugin-sdk.d.ts and manifest.schema.json first. Use minimum permissions. SDK 1.6 platform features require the unreleased new host; simulator success does not prove installed-client support.
Edit only this plugin. Preserve scripts/ and sdk/plugin-sdk/ layout. Do not modify the SDK to bypass checks. Support --awd-* theme tokens, keyboard interaction, narrow panels and light/dark themes.
Declare views and installation/user/project settings as needed. Credentials must use trusted host connections; never put tokens in the iframe, logs, repository or zip.
Choose shared host AI/speech/tasks/clipboard with the required permissions, or fixed HTTPS BYO operations with network permission. MCP must use public HTTPS Streamable HTTP and explicitly declared tools with the supported schema subset. No stdio, private hosts, arbitrary URLs or system settings.
Use synthetic simulator data to verify success, permission denial, disconnect, retry and cancel. Do not access customer data, real login or paid services. Use only my designated isolated project for host installation; ask the host administrator to grant service trust in settings after manifest changes.
Run node scripts/plugin-dev.mjs validate . and node scripts/plugin-dev.mjs package . --out ../my-plugin.zip. Verify no .env, credentials, private keys or host trust markers. Report changes, checks, unverified boundaries and zip location.
Only submit when I explicitly request it, using a locally provided AWD_PLUGIN_TOKEN without printing it. Report pending review accurately; never claim approval, payment or release.

MCP Streamable HTTP specification

Готово к отправке

Запакуйте в zip и отправьте — мы проверим его как можно скорее.