Plugin structure
Under the hood, a .lia file is a ZIP archive with a fixed structure. Liatir validates the bundle signature before reading the manifest or executing any runtime payload.
Runtime variants
There are three supported runtimes: Node, Python, and WASM. All three declare the I/O contract the same way — once, in the code, with the define_plugin API — and liatir build generates the bundle manifest schema from it. There is no schema to keep in sync by hand:
- Node declares it with
definePlugin({...})from@liatir/api. - Python declares it with
define_plugin(...)from the CLI-managedliatirmodule scaffolded next to the entry point. - WASM declares it with
define_plugin()from the CLI-managedsrc/liatir.rsmodule.
Node plugins have no .lia-manifest.json at all (metadata lives in package.json). Python and WASM projects keep a small .lia-manifest.json for metadata (name, version, description, category, tags) — plus, for Python, the runtime spec (entry point, packages, requirements). Legacy Python/WASM plugins that still declare inputSchema/outputSchema in the manifest keep building; when a code contract exists, it wins and the manifest schema is ignored.
Generated Node manifest
For Node plugins, the manifest is generated by liatir build after bundling and validating the default export.
Manifest example:
{
"name": "my-plugin",
"version": "1.0.0",
"description": "What this plugin does",
"runtime": "node",
"category": "Utilities",
"tags": ["text"],
"inputSchema": {
"text": {
"type": "string",
"label": "Text",
"description": "Field description.",
"required": true,
"default": "hello from Liatir"
}
},
"outputSchema": {
"length": {
"type": "number",
"label": "Length",
"description": "Field description.",
"format": "integer"
}
}
}Node entry point (index.ts or .js)
Keep the entry point clean and do not alter its structure, or Liatir will reject the plugin. Organize your source files in the project's src/ folder and keep the entry point as a thin wrapper that exports the plugin logic.
import { definePlugin, field, type PluginContext } from "@liatir/api";
const liatirPlugin = definePlugin({
inputs: {
text: field.string({
label: "Text",
description: "Text to analyze.",
required: true,
default: "hello from Liatir",
}),
},
outputs: {
length: field.number({
label: "Length",
description: "Number of characters in the input text.",
format: "integer",
}),
},
});
export default liatirPlugin.main(async ({ input, Liatir }: PluginContext<typeof liatirPlugin>) => {
// Your logic
return {
length: input.text.length,
};
});liatir build rejects Node plugins that export an invalid contract.
Field schemas
Input fields can be of type:
stringnumberbooleanfile
Output fields can be of type:
stringnumberbooleanfilestatsjson
Shared field properties include:
| Property | Description |
|---|---|
label | Human-readable label shown in the UI. |
description | Short explanation shown near the field. |
required | Whether the field must be set before running. |
default | Default value applied by the UI and by liatir dev. |
accept | Accepted file extensions for file inputs. |
ext | Expected file extensions for file outputs. |
format | Numeric output display hint: integer, decimal, percent, or bytes. |
File outputs
If an output field has type: "file", the returned value can either reference an existing file path or provide inline content for Liatir to persist.
return {
report: {
content: "sample,score\nA,0.92\n",
fileName: "report.csv",
},
};Liatir registers saved file outputs under the workspace's Results folder so they can be opened later or connected to downstream pipeline steps.
WASM contract and manifest
WASM plugins declare the contract in src/main.rs with the CLI-managed src/liatir.rs module (liatir init --wasm scaffolds both, liatir build keeps the module in sync):
mod liatir;
use liatir::{define_plugin, field};
use serde_json::json;
fn main() {
define_plugin()
.input("text", field::string()
.label("Text")
.required(true)
.default_value("hello from Liatir"))
.output("length", field::number()
.label("Length")
.integer())
.main(|ctx| {
let text = ctx.str("text")?;
Ok(json!({ "length": text.chars().count() }))
});
}.lia-manifest.json only carries metadata:
{
"name": "wasm-length",
"version": "1.0.0",
"description": "Count characters in text.",
"runtime": "wasm",
"category": "Utilities",
"tags": ["text"]
}liatir build compiles the crate, reads the contract back from the binary, and generates the bundle manifest schema from it. At run time the SDK validates the input before your handler runs and the output after it returns; stdout is reserved for the result JSON (use eprintln! for logs). Legacy tools without the SDK keep working with inputSchema/outputSchema in the manifest.
Python contract and manifest
Python plugins declare the contract in the entry module with the CLI-managed liatir module (liatir init --python scaffolds src/liatir.py next to src/main.py; liatir build keeps it in sync and ships it in the bundle):
from liatir import define_plugin, field
plugin = define_plugin(
inputs={
"text": field.string(
label="Text",
required=True,
default="hello from Liatir",
),
},
outputs={
"length": field.number(label="Length", format="integer"),
},
)
@plugin.main
def main(ctx):
return {"length": len(ctx.input["text"])}The handler receives a context with the validated input (defaults applied, required and typed fields checked); the returned dict is validated against the declared outputs. .lia-manifest.json carries metadata and the Python runtime spec:
{
"name": "python-length",
"version": "1.0.0",
"description": "Count characters with Python.",
"runtime": "python",
"python": {
"entry": "src/main.py",
"pythonRequirement": {
"minVersion": "3.10",
"maxVersionExclusive": "3.13",
"label": "Python >=3.10,<3.13"
},
"packages": [],
"requirements": []
}
}When the packaged .lia runs, Liatir creates an isolated managed Python runtime for that plugin bundle and installs the declared packages or requirements there. Legacy plugins with a plain main(input) function and a manifest-owned inputSchema/outputSchema keep working.