Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Packaging

A node is either a folder on its own or a member of a package. You want a package when several nodes share code.

A bare node

A directory with a metadata.json at its root. It stands alone.

nodes/reply/
  metadata.json
  mod.rs
  deps.toml        optional
  tests.rs         optional

A package

A directory with a package.toml at its root. Its members are auto-detected: every immediate subdirectory holding a metadata.json. Adding a node is adding a folder.

nodes/slack/
  package.toml
  api.rs                      shared code, reached as `super::api`
  metadata.json               optional PARTIAL: defaults every member inherits
  send_message/
    metadata.json
    mod.rs
    tests.rs
  receive_message/
    metadata.json
    mod.rs

package.toml carries the package name and the cargo dependencies its members share:

[package]
name = "slack"

[dependencies]
async-trait = "0.1"
serde_json = "1"
uuid = { version = "1", features = ["v4"] }

Any .rs file at the package root is shared code, reached from a member as use super::<filename>;. That is where the API wrapper goes, and where a package defines its own provider meter when its nodes call a paid service weft does not ship.

Nesting and discovery

The catalog walk recurses until it hits a unit, meaning a directory with either a metadata.json or a package.toml, and then stops descending. So units may sit at any depth (catalog/ai/llm/anthropic/), and a unit never nests inside a unit.

Symlinks are never followed, and target, node_modules, .git and .weft are skipped. Two units declaring the same node type is a loud collision rather than a last-one-wins.

What gets compiled

Only what your program actually uses.

The compiler reads every node’s metadata.json without compiling any node Rust, which is what makes the editor’s live feedback fast. Codegen then emits one cargo crate per referenced package, containing only the referenced nodes, plus a registry mapping node type names to implementations.

So a project using three nodes out of the whole catalog compiles three nodes. Nothing scans the filesystem at run time; the generated code names exactly what it needs.

Dependencies

deps.toml next to a mod.rs, for that one node:

[dependencies]
reqwest = { version = "0.12", features = ["json"] }

[build-dependencies]
cc = "1"

[system.build.apt]
default = ["pkg-config", "libssl-dev"]

[system.runtime.apt]
default = ["ca-certificates"]

[build.env]
SOME_PATH = "{{catalog_path}}/vendor"

Always available without declaring anything: weft, tokio, serde, serde_json, async-trait, anyhow, tracing, uuid.

[system.*] entries declare OS packages the node needs, keyed by package manager and optionally by distro version. That is what lets a node carry a native dependency without every user hand-installing it.

Comment each dependency with why it is there.

Package-level metadata

A package root may hold a partial metadata.json of defaults every member inherits, which is how a package’s nodes share a types block or a provider name: Package defaults.

Sharing a package

Copy the folder. A package is self-contained on disk, so putting one in your project’s nodes/ is the whole install.

Nothing pulls a package from git for you yet. That command is an open contribution slot, and the shape it should take is in CONTRIBUTING.

Whatever gets built has to hold one property. A project’s nodes/ is the complete list of what its programs can do, and nothing outside the project folder is reached during a build. That is what makes a project directory portable, and what stops an upgrade changing what an existing program does.