Skip to content
Download

Build a plugin ​

A plugin is a separate program. QueryLane starts the command from the plugin's plugin.json and talks JSON-RPC 2.0 to it over stdin and stdout, one JSON message per line. You can write it in any language. The official plugins and the tools that build, sign and publish them live in the querylane-plugins repository.

QueryLane installs only packages signed with the official QueryLane key. You can build and test a package signed with another key, but the app refuses to install it.

Set up the repository ​

  • Node 24, then npm install.
  • A checkout of the QueryLane app at ../dataflow3/roadmap, or QUERYLANE_APP_DIR pointing at it. Tests run plugins through the app's own plugin host.
  • For npm run build, an Ed25519 private key in PEM at ~/.querylane/plugin-signing/querylane-official.pem, or its path in QUERYLANE_PLUGIN_SIGNING_KEY.

Create the plugin ​

  1. Create plugins/<name>/ with plugin.json and your code, and optionally an icon and a guide for the AI assistant.
  2. Add the folder to PLUGIN_SOURCES in tools/lib/config.mjs with its platforms, for example { dir: 'plugins/hello', platforms: ['darwin-aarch64'] }. The manifest test fails for a folder that is not listed.
  3. Give every plugin English and Russian texts with the same keys; the manifest test checks it.

plugin.json ​

FieldMeaning
schemaVersion, apiVersionBoth 1
idLowercase dotted id, such as com.example.hello
version, minAppVersion1.2.3; the oldest QueryLane version the plugin needs
author{ "name": "…", "url": "…" }
iconSquare SVG or PNG in the package, up to 128 KiB
permissionsAny of network, download, spawnProcesses, userFiles
platformsKeys such as darwin-aarch64, each with command, optional setup commands and env
contributes.stepsid, language of the step code, starterCode, and template: true to substitute ${...} values before the plugin gets the code
contributes.settingskey, type, default, group, scope and more
contributes.toolsname, description, inputSchema (JSON Schema)
contributes.runEvents["finished"] to receive run notifications
contributes.guideMarkdown file with instructions for writing step code
locales, defaultLocaleTexts per language, and the fallback language

Commands and env values can use ${pluginDir} (the unpacked package), ${dataDir} (a folder that survives updates) and ${node} (the Node.js that QueryLane runs). setup commands run once before the first start and after updates.

Setting types are string, number, boolean, enum, stringList, secret and proxyProfile. scope is plugin (default), step or notification.

Each locale needs name and description, plus flat keys: steps.<id>.title, settings.<key>.label, settings.<key>.description, settings.<key>.options.<option>, groups.<group> and errors.<reasonCode>.

Protocol ​

QueryLane sends these requests:

MethodAnswer
initialize{ "apiVersion": 1 }
prepareRuns once after install and update, for example to download a browser
step/run{ "summary": {...}, "rows": [...] } for code, settings, inputs, variables, timeoutMs, artifactsDir and more
tool/call, tool/close{ "data": ..., "images": [...] } for a tool call; tool/close ends a tool session
run/finishedReports a full run of a job that lists the plugin in its notifications
shutdownSent before QueryLane stops the plugin

While it handles a request, the plugin can send the notifications rows, progress and artifact (kind is screenshot, html or file) with requestId set to the request id. step/cancel arrives as a notification with the runId to stop.

Report failures as JSON-RPC errors with data.reasonCode. QueryLane shows the plugin's errors.<reasonCode> text. Keep stdout for protocol messages and write logs to stderr. Exit when stdin closes.

Minimal example ​

plugins/hello/plugin.json:

json
{
  "schemaVersion": 1,
  "id": "com.example.hello",
  "version": "1.0.0",
  "apiVersion": 1,
  "author": { "name": "Example" },
  "permissions": [],
  "platforms": {
    "darwin-aarch64": { "command": ["${node}", "${pluginDir}/main.mjs"] }
  },
  "contributes": {
    "steps": [{ "id": "greet", "language": "plaintext", "template": true, "starterCode": "Hello, ${name}" }]
  },
  "locales": {
    "en": { "name": "Hello", "description": "Returns a greeting as a row", "steps.greet.title": "Greeting" },
    "ru": { "name": "Привет", "description": "Возвращает приветствие строкой", "steps.greet.title": "Приветствие" }
  }
}

plugins/hello/main.mjs:

js
import readline from 'node:readline';

const send = message => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);

readline.createInterface({ input: process.stdin }).on('line', (line) => {
  const { id, method, params = {} } = JSON.parse(line);
  if (id === undefined)
    return;
  if (method === 'initialize')
    send({ id, result: { apiVersion: 1 } });
  else if (method === 'step/run')
    send({ id, result: { rows: [{ greeting: params.code.trim() }] } });
  else if (method === 'prepare' || method === 'shutdown')
    send({ id, result: {} });
  else
    send({ id, error: { code: -32601, message: `unknown method ${method}`, data: { reasonCode: 'HELLO_METHOD_UNKNOWN' } } });
});

Build and test ​

  • npm run build packs every listed plugin into dist/<id>-<version>-<platform>.qlplugin, a gzip tar with plugin.json at its root, signs it and writes dist/catalog.json. Without the official key it warns that the app will refuse the packages.
  • npm run check runs the linters, the manifest check against the app's validator, and the package and plugin tests.

To try the plugin end to end, write a test in tests/ like the existing ones: build the package with a throwaway Ed25519 key, install it into a scratch folder with the app's installPluginPackage and that key in trustedKeys, then call preparePlugin and runPluginStep from the app's plugins/host.ts.

With a development checkout of the app, its npm run dev offers the packages from dist/catalog.json in Marketplace. They still need the official signature.

Other runtimes ​

  • Bun: a package.json with querylanePlugin.bun (version, entry, binary) is compiled into one executable, so users install nothing. The build needs Bun at that version.
  • Python: bundleUv: true in PLUGIN_SOURCES adds bin/uv to the package, and setup commands use it to install Python and dependencies into ${dataDir}, as the Browser plugin does.