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, orQUERYLANE_APP_DIRpointing 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 inQUERYLANE_PLUGIN_SIGNING_KEY.
Create the plugin
- Create
plugins/<name>/withplugin.jsonand your code, and optionally an icon and a guide for the AI assistant. - Add the folder to
PLUGIN_SOURCESintools/lib/config.mjswith its platforms, for example{ dir: 'plugins/hello', platforms: ['darwin-aarch64'] }. The manifest test fails for a folder that is not listed. - Give every plugin English and Russian texts with the same keys; the manifest test checks it.
plugin.json
| Field | Meaning |
|---|---|
schemaVersion, apiVersion | Both 1 |
id | Lowercase dotted id, such as com.example.hello |
version, minAppVersion | 1.2.3; the oldest QueryLane version the plugin needs |
author | { "name": "…", "url": "…" } |
icon | Square SVG or PNG in the package, up to 128 KiB |
permissions | Any of network, download, spawnProcesses, userFiles |
platforms | Keys such as darwin-aarch64, each with command, optional setup commands and env |
contributes.steps | id, language of the step code, starterCode, and template: true to substitute ${...} values before the plugin gets the code |
contributes.settings | key, type, default, group, scope and more |
contributes.tools | name, description, inputSchema (JSON Schema) |
contributes.runEvents | ["finished"] to receive run notifications |
contributes.guide | Markdown file with instructions for writing step code |
locales, defaultLocale | Texts 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:
| Method | Answer |
|---|---|
initialize | { "apiVersion": 1 } |
prepare | Runs 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/finished | Reports a full run of a job that lists the plugin in its notifications |
shutdown | Sent 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:
{
"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:
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 buildpacks every listed plugin intodist/<id>-<version>-<platform>.qlplugin, a gzip tar withplugin.jsonat its root, signs it and writesdist/catalog.json. Without the official key it warns that the app will refuse the packages.npm run checkruns 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.jsonwithquerylanePlugin.bun(version,entry,binary) is compiled into one executable, so users install nothing. The build needs Bun at thatversion. - Python:
bundleUv: trueinPLUGIN_SOURCESaddsbin/uvto the package, andsetupcommands use it to install Python and dependencies into${dataDir}, as the Browser plugin does.