Package Format
This page is the canonical reference for the .cloacina package format: the
archive layout, the package.toml manifest schema, and how the server/daemon
load a package. It applies to both Rust and Python packages.
A .cloacina package is a bzip2-compressed tar archive of source, not a
compiled binary. Inside, everything lives under a single top-level directory
named <name>-<version>/. That directory always contains a package.toml
manifest plus the workflow source:
- Rust —
Cargo.toml,build.rs, andsrc/(the cdylib is compiled by the server’s compiler at load time, not shipped in the archive). - Python — a
workflow/directory holding your module tree (imported at load time; nothing is compiled), plus an optionalvendor/directory for third-party dependencies.
The server reads package.toml to learn the package’s identity and language,
then compiles (Rust) or imports (Python) the source to discover tasks.
package.toml is the manifestThere is nomanifest.json.package.tomlis the only manifest the server reads. It is parsed byfidius_core::package::load_manifestinto theCloacinaMetadataschema described below.
analytics-workflow-1.0.0.cloacina (bzip2 tar)
└── analytics-workflow-1.0.0/
├── package.toml
├── Cargo.toml
├── build.rs # calls cloacina_build::configure()
└── src/
└── lib.rs # #[task] / cloacina::package!()
data-pipeline-1.0.0.cloacina (bzip2 tar)
└── data-pipeline-1.0.0/
├── package.toml
├── workflow/ # REQUIRED — module tree lives here
│ └── data_pipeline/
│ ├── __init__.py
│ └── tasks.py # @cloaca.task / @cloaca.trigger
└── vendor/ # optional — vendored dependencies
For Python, the module tree must live under workflow/; a top-level module
is rejected at load with Missing workflow source directory. The vendor/
directory (if present) and workflow/ are both added to sys.path before
import. See Packaging Python Workflows
for the full Python procedure.
Every package has a top-level package.toml with two tables: [package]
(identity) and [metadata] (workflow descriptor).
[package]
name = "data-pipeline"
version = "1.0.0"
interface = "cloacina-workflow-plugin"
interface_version = 1
extension = "cloacina"
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Package name |
version |
string | yes | Semantic version |
interface |
string | yes | Plugin interface — "cloacina-workflow-plugin" |
interface_version |
integer | yes | Interface version — currently 1 |
extension |
string | yes | Archive extension — "cloacina" |
[metadata] deserializes into CloacinaMetadata
(crates/cloacina-workflow-plugin/src/types.rs), which is a closed schema
(#[serde(deny_unknown_fields)]). Unknown keys are rejected at upload/load.
# Rust
[metadata]
language = "rust"
workflow_name = "analytics_workflow"
description = "ETL pipeline for analytics data"
author = "you@example.com"
# Python
[metadata]
language = "python"
workflow_name = "data_pipeline"
entry_module = "data_pipeline.tasks"
description = "ETL pipeline for analytics data"
author = "you@example.com"
requires_python = ">=3.10"
| Field | Type | Required | Description |
|---|---|---|---|
language |
string | yes | "rust" or "python" |
workflow_name |
string | for workflows | The workflow name (Rust may source this from #[workflow(name = …)]) |
graph_name |
string | for computation graphs | Graph name — set instead of workflow_name for CG packages |
entry_module |
string | Python only | Dotted module path relative to workflow/ the loader imports |
requires_python |
string | no | PEP 440 specifier, e.g. ">=3.10" (Python only) |
description |
string | no | Human-readable description |
author |
string | no | Author/owner |
reaction_mode |
string | computation graphs | "when_any" or "when_all" |
input_strategy |
string | computation graphs | "latest" or "sequential" |
accumulators |
array | computation graphs | [[metadata.accumulators]] source configs |
Rejected keyspackage_typeand[[metadata.triggers]]are hard-rejected by the closed schema. Package classification (workflow vs. computation graph vs. reactor) flows through FFI metadata for Rust and the presence ofgraph_namefor Python. Triggers are declared in code (#[trigger]in Rust,@cloaca.triggerin Python), never in the manifest.
- The server/daemon receives the archive (upload, or the daemon’s watch directory) and extracts it.
- It reads
package.tomland dispatches on[metadata].language. - Rust — the compiler runs
cargo buildon the unpacked source, then loads the resulting cdylib and calls its FFI entry points to enumerate tasks. Python — the loader addsworkflow/(andvendor/) tosys.pathand importsentry_module; the@cloaca.task/@cloaca.triggerdecorators register tasks and triggers as a side effect of import. - Discovered tasks/triggers are registered and the workflow becomes available.
Because discovery happens by building/importing source, registration must
occur at import/macro-expansion time — not behind if __name__ == "__main__" or
inside a function body.
Rust packages don’t hand-write #[no_mangle] symbols. cloacina::package!()
(invoked at crate root) and the #[plugin_impl] machinery emit the fidius plugin
registry plus the entry points the host calls: get_task_metadata,
execute_task, and the computation-graph/reactor/trigger metadata accessors.
Fidius validates the interface hash before any call.
# Compile the workflow cdylib (wrapper around cargo build)
cloacinactl package build .
# Pack the source directory into a .cloacina archive
cloacinactl package pack . --out analytics-workflow-1.0.0.cloacina
pack requires a package.toml in the directory and archives the source; it
does not compile. (Compilation happens at load time on the server.)
cloacinactl package pack . --out data-pipeline-1.0.0.cloacina
pack reads [metadata].language; for python it skips cargo, validates the
workflow/ layout (that the directory exists and entry_module resolves under
it), vendors declared dependencies if applicable, and archives the source. A
mis-laid-out package fails at pack time rather than at upload. package build is
a no-op for Python (there is nothing to compile). The archive can also be built
by hand — see
Packaging Python Workflows.
# Validate + inspect a local archive (unpacks it and checks the manifest)
cloacinactl package validate analytics-workflow-1.0.0.cloacina
# Manual — it's just a bzip2 tar
tar -tjf analytics-workflow-1.0.0.cloacina # list contents
tar -xjOf analytics-workflow-1.0.0.cloacina \
analytics-workflow-1.0.0/package.toml # read the manifest
cloacinactl package validate <file>operates on a local.cloacinaarchive.cloacinactl package inspect <id>is different — it queries a running server for an already-registered workflow by id, not a local file.
- Creating Your First Package — scaffold/validate/pack/upload with
package new - Packaging Python Workflows — the full Python procedure
- Packaged Triggers — triggers in a packaged Python workflow
- FFI System — how the host calls into compiled packages
- Packaged Workflow Architecture — load/registration internals