Automate
Plugins
Add your own actions to a space: an HTTPS service in any language, or a command on your own machine through a runner. Signed jobs, ranged reads of the input, results as sidecars and files.
A plugin adds actions to Side Space: a QC check, your own transcription service, an upscaler on your GPU. Its code never runs on the Side Space server. It is either an HTTPS service that receives signed jobs, or a runner: a command on a machine of yours that claims jobs, with no public address needed.
Once a space manager installs it, its actions appear as x-acme.qc.check next to the built-in ones. They can be run by hand, used in pipelines, called by agents over MCP, and are cached by content the same way (within the space, and run again when the plugin's version changes).
The manifest
{
"name": "x-acme.qc",
"title": "Acme QC",
"version": "1.2.0",
"endpoint": "https://qc.acme.example/side/jobs",
"timeout_seconds": 1800,
"actions": [
{
"name": "check",
"title": "Check for dropped frames",
"accepts": ["video/"],
"writes": ["x-acme.qc"],
"params_schema": { "type": "object", "properties": { "strict": { "type": "boolean" } } }
},
{ "name": "report", "title": "Make a QC report", "accepts": ["video/"], "writes": ["x-acme.qc", "files"] }
]
}namestarts withx-; actions are called<name>.<action>.acceptslists MIME prefixes (empty means any file). Params are checked againstparams_schemabefore a job is sent.writesis everything the action may write:x-…namespaces, and if declaredtranscript,ai.tags,ai.generation,userandfiles(new files, which get aderivedsidecar). A result that writes anything else fails as a whole and writes nothing.versionis part of the cache key: a new version processes files again.
sidespace plugin install "Film A" https://qc.acme.example/side/manifest.json # prints the signing secret, once
sidespace plugin list "Film A"
sidespace run "Film A" /Ep01/a.mov x-acme.qc.check --param strict=true
sidespace plugin enable "Film A" plg_… --off
sidespace plugin rm "Film A" plg_…HTTPS plugins
The server POSTs a job to endpoint, signed like a webhook (Side-Signature, with the secret from install):
{
"type": "run",
"run_id": "run_…",
"action": "check",
"attempt": 1,
"space_id": "spc_…",
"node_id": "nod_…",
"version_id": "ver_…",
"path": "/Ep01/a.mov",
"name": "a.mov",
"mime": "video/quicktime",
"size": 734003200,
"params": { "strict": true },
"sidecars": { "media": { "duration_s": 92.4, "width": 3840, "height": 2160, "fps": 24 } },
"input": { "url": "https://blocks.side.example.com/files/ver_…?t=…", "expires_at": "…" },
"callback": { "url": "https://side.example.com/v1/plugin-runs/run_…", "token": "srt_…", "expires_at": "…" }
}input.url reads the version's bytes with Range support, valid for the whole timeout, so you can read only the part you need. Answer within 60 seconds:
| Answer | Means |
|---|---|
200 with a result {"sidecars": {"x-acme.qc": {…}}, "files": […]} | Done |
202 | You're working in the background; finish through the callback |
4xx | The run fails with the reason you give |
5xx or a timeout | Retried like any action |
After a 202, call the callback with Authorization: Bearer <callback.token>:
POST {callback.url}/progresswith{"progress": 0.4}POST {callback.url}/uploadfor an upload ticket, then POST the file to the gateway's/stream-uploadto get its manifestPOST {callback.url}/completewith the result, or{"error": "…", "retry": true}
If a run isn't completed within timeout_seconds, it is sent again, up to three times.
import { PluginRun } from "@sidespace/sdk";
import { verifyWebhookSignature } from "@sidespace/sdk/webhooks";
// In your HTTP handler, with the raw body:
if (!verifyWebhookSignature(secret, req.headers["side-signature"], rawBody)) return res.writeHead(401).end();
res.writeHead(202).end();
const run = new PluginRun(JSON.parse(rawBody));
await run.progress(0.5);
const manifest = await run.upload(reportBytes);
await run.complete({ sidecars: { "x-acme.qc": { passed: true } }, files: [{ path: "/Ep01/a.qc.pdf", manifest }] });A complete reference plugin using only the Python standard library (answering at once, answering 202 and calling back, uploading a file, checking signatures) is in the repository at tests/e2e/fakeplugin.py.
Runners: plugins on your own machine
When you'd rather not run an HTTPS service, or the work needs local models and GPUs (whisper.cpp, a local vision model, grading tools), set "runtime": "runner" instead of an endpoint and run this on that machine:
sidespace plugin install "Film A" ./local-tools.json # note the plg_… id and whsec_… secret
sidespace runner --plugin plg_… --secret whsec_… --jobs 2 -- ./qc.sh- The runner long-polls the server for jobs, so the machine needs no public address: behind NAT or on a laptop is fine. Jobs queue while it is away.
- Your command runs once per job. The input file is already downloaded at
SIDE_INPUT; the job JSON is inSIDE_JOB(and on stdin);SIDE_ACTIONandSIDE_PARAMShold the action and params; write new files intoSIDE_OUTPUT. - Print the result JSON on stdout. The runner uploads the files it names and completes the run:
{ "sidecars": { "x-acme.qc": { "passed": true } }, "files": [{ "path": "/Ep01/qc.pdf", "file": "qc.pdf", "mime": "application/pdf" }] }progress 0.4(orprogress: 40%) on stderr shows as progress; other output is passed through. Exit code75means "try again later"; any other non-zero code fails the run with the last lines of stderr as the reason.- If no runner claims a job within
timeout_seconds, it is queued again, and fails after three tries saying no runner came.plugin listshows when a runner last checked in.
Runners in other languages call POST /v1/plugin-runs/claim?wait=25 with Authorization: Bearer plg_…:whsec_…, get the same job an HTTPS plugin would, and report through its callback.