Skip to content

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-managed liatir module scaffolded next to the entry point.
  • WASM declares it with define_plugin() from the CLI-managed src/liatir.rs module.

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:

json
{
  "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.

ts
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:

  • string
  • number
  • boolean
  • file

Output fields can be of type:

  • string
  • number
  • boolean
  • file
  • stats
  • json

Shared field properties include:

PropertyDescription
labelHuman-readable label shown in the UI.
descriptionShort explanation shown near the field.
requiredWhether the field must be set before running.
defaultDefault value applied by the UI and by liatir dev.
acceptAccepted file extensions for file inputs.
extExpected file extensions for file outputs.
formatNumeric 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.

ts
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):

rust
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:

json
{
  "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):

python
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:

json
{
  "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.