Signal Studio is in public beta. Usable, but still moving. Join the beta

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:

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

dandi.zip
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

extension.json
{
  "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" }]
  }
}
FieldRequiredMeaning
idyesUnique, reverse-domain style. Never changes.
nameyesShown in the extensions list
versionyesSemantic version, three numbers
min_appyesMinimum Signal Studio version. Below it, the extension is listed as incompatible instead of loading.
authoryesName and a URL people can check
contributes.panelsnoPanels added to the side rail
contributes.scriptsnoScripts 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.

panel.html
javascript
// 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.

search.py
python
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.

terminal
# 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

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.