Appearance
Plugin context
Every plugin handler receives a context carrying the validated input. What else it carries depends on the runtime.
Node — PluginContext
PluginContext<typeof liatirPlugin> is the recommended TypeScript helper for typing the .main(...) handler. It gives you:
input, inferred fromdefinePlugin({ inputs });Liatir, the Node plugin bridge to the running Liatir app.
ts
import { definePlugin, field, type PluginContext } from "@liatir/api";
const liatirPlugin = definePlugin({
inputs: {
text: field.string({ label: "Text", required: true }),
},
outputs: {
length: field.number({ label: "Length", format: "integer" }),
},
});
export default liatirPlugin.main(async ({ input, Liatir }: PluginContext<typeof liatirPlugin>) => {
await Liatir.deps.check("node");
return { length: input.text.length };
});@liatir/api also exports PluginInput<S> and PluginOutput<S>, but most plugins only need PluginContext<typeof liatirPlugin>. The type is erased at runtime; the contract is enforced at build time and through TypeScript.
WASM plugins are fully sandboxed and does not have the API bridge.
Python — ctx
The @plugin.main handler receives a context with the validated input on ctx.input (a dict; declared defaults are already applied). ctx.raw_input holds the original payload before validation.
python
from liatir import define_plugin, field
plugin = define_plugin(
inputs={"text": field.string(label="Text", required=True)},
outputs={"length": field.number(label="Length", format="integer")},
)
@plugin.main
def main(ctx):
return {"length": len(ctx.input["text"])}Python plugins also receive the Liatir bridge, exposed on the context as ctx.liatir. It mirrors the Node bridge namespaces with snake_case method names (ctx.liatir.jobs, ctx.liatir.deps, ctx.liatir.desktop.*, ctx.liatir.invoke), and reaches the app over the same local IPC server:
python
@plugin.main
def main(ctx):
ctx.liatir.deps.check("node")
return {"length": len(ctx.input["text"])}The connection is resolved lazily, so plugins that never touch the bridge run without requiring the app to be reachable.
WASM — ctx
The Rust handler receives a &Context with typed getters over the validated input, so a wrong-typed or missing input surfaces as an error you can ?:
| Getter | Returns |
|---|---|
ctx.str(name) | &str |
ctx.string(name) | String |
ctx.i64(name) / ctx.f64(name) | integer / float |
ctx.bool(name) | bool |
ctx.path(name) | PathBuf of a file input |
ctx.get(name) | raw &serde_json::Value |
rust
mod liatir;
use liatir::{define_plugin, field};
use serde_json::json;
fn main() {
define_plugin()
.input("text", field::string().label("Text").required(true))
.output("length", field::number().label("Length").integer())
.main(|ctx| {
let text = ctx.str("text")?;
Ok(json!({ "length": text.chars().count() }))
});
}ctx.storage_dir() (/storage) is persistent per-plugin storage and ctx.work_dir() (.) is the temporary job directory mounted by the sandbox.
Runtime validation
In Python and WASM, the SDK validates the input before the handler runs (defaults applied, required and typed fields checked) and the output after it returns (JSON, and no key outside the declared outputs). In Node, validation happens at build time — liatir build and liatir dev inspect the exported plugin shape after bundling — not on each run.