Writing an extension
An extension adds a panel to Signal Studio. What goes in that panel is entirely yours: the application gives you a surface and a way to run Python, and stays out of the way after that.
Cards and extensions are different things
A card is a processing step on the canvas: a manifest, an implementation, typed inputs and outputs. If you want to add a filter, a metric or a decomposition, you want a card, not an extension.
An extension adds a panel to the side rail. Use it when you need a surface the canvas cannot give you: a data source browser, a custom viewer, a connection to an external archive, a tool that has its own interface.
Two primitives
An extension can do exactly two things, and everything else is built on top of them:
- Contribute a panel, rendered as a webview from HTML you ship.
- Ship Python scripts that the panel can call.
The panel is a webview rather than a React component on purpose: you cannot compile against the application's build, and you should not have to. You ship HTML, you get a message API, and what you do inside is your business.
Package structure
extension.json the manifest, at the root of the archive panel.html your interface icon.svg the side rail icon, readable at 24px search.py a script the panel can call README.md optional but expected
extension.json must sit at the root of the
zip. The installer looks for it there; a manifest nested one folder deeper
will not be found.The manifest
{
"id": "neuracrypt.dandi",
"name": "DANDI Archive",
"version": "1.0.0",
"min_app": "0.9.0",
"author": { "name": "NeuraCrypt", "url": "https://eeg.studio" },
"contributes": {
"panels": [{
"id": "dandi",
"title": "DANDI",
"icon": "icon.svg",
"entry": "panel.html",
"placement": "sideRail"
}],
"scripts": [{ "id": "search", "entry": "search.py" }]
}
}| Field | Required | Meaning |
|---|---|---|
id | yes | Unique, reverse-domain style. Never changes. |
name | yes | Shown in the extensions list |
version | yes | Semantic version, three numbers |
min_app | yes | Minimum Signal Studio version. Below it, the extension is listed as incompatible instead of loading. |
author | yes | Name and a URL people can check |
contributes.panels | no | Panels added to the side rail |
contributes.scripts | no | Scripts the panel may call, by id |
placement currently accepts sideRail only. Other
placements may follow; declaring an unknown one is ignored rather than
fatal.
The webview API
Inside your panel.html, the application exposes exactly three
things. Nothing else, on purpose: a small surface is one you can rely on
across versions.
// Call a script declared in contributes.scripts const result = await window.signalStudio.call("search", { query: "eeg", limit: 20 }); // Who you are, and which version you are running on window.signalStudio.extensionId // "neuracrypt.dandi" window.signalStudio.version // the application version
call() returns a promise resolving to whatever your script
printed. It rejects if the script fails, times out, or is not declared in the
manifest. Handle the rejection: a panel that stays on a spinner forever is
the most common bug in this kind of code.
The Python contract
Your script receives the payload as a JSON file whose path is
argv[1], and prints one JSON object on stdout. Nothing
else on stdout, or the parse fails.
import json, sys, urllib.request def main(): payload = json.load(open(sys.argv[1], encoding="utf-8")) query = payload.get("query", "") req = urllib.request.Request("https://api.example.org/search?q=" + query) # Send a real User-Agent. A default library signature may be refused. req.add_header("User-Agent", "MyExtension/1.0 (+https://example.org)") try: with urllib.request.urlopen(req, timeout=20) as r: data = json.loads(r.read()) print(json.dumps({"ok": True, "results": data})) except Exception as e: # Always answer JSON, even on failure. print(json.dumps({"ok": False, "error": str(e)})) if __name__ == "__main__": main()
The script runs on the interpreter bundled with Signal Studio, and your
package directory is on the PYTHONPATH, so you can import your
own modules. Scripts time out after 120 seconds. There is no streaming yet:
one call, one answer.
Testing locally
You do not need to publish anything to try an extension. Drop the folder, or a zip of it, onto the Signal Studio window, or use File / Install extension…. It installs into your user extensions directory and the panel appears in the side rail.
| Windows | %APPDATA%\Signal Studio\extensions\user\ |
|---|---|
| macOS | ~/Library/Application Support/Signal Studio/extensions/user/ |
| Linux | ~/.config/Signal Studio/extensions/user/ |
You can edit files in place there and reload. If your manifest is invalid or
min_app is too high, the extension shows up in
Preferences / Extensions with an error and the reason, rather than
silently not loading.
Publishing
Zip the folder so that extension.json is at the root, then
submit it with kind = extension on the
publishing page. Submissions go through
review before they appear in the public marketplace.
# Windows Compress-Archive -Path my-extension/* -DestinationPath my-extension-1.0.0.zip # macOS and Linux cd my-extension && zip -r ../my-extension-1.0.0.zip .
What reviewers look at
- The manifest is valid and the id is not already taken by someone else.
- The Python does what the description says, and nothing else. Network calls to a service you did not mention will be rejected.
- No obfuscated code, no downloading and running further code at runtime.
- Errors are handled: a failing request produces a readable message, not a stuck panel.
- The description tells a reader what the extension does before they install it.
A worked example
The DANDI Archive extension is a complete, working example: a side rail panel that searches an external neuroscience archive and links out to it. Its source is a reasonable starting point for your own.