This guide explains how to create workflow projects, add workflows to a project, write workflow definitions, and call one workflow from another.
A LightFlow workflow is a reusable Rust library crate that exposes:
pub fn define() -> WorkflowSpecThe workflow source lives in src/lib.rs. LightFlow statically parses the Rust
builder DSL from that file; it does not compile or execute workflow source when
discovering workflow metadata.
There are two common shapes:
- Leaf workflow: declares input/output ports and optionally a runtime capability, but has no graph nodes.
- Composite workflow: declares graph nodes that reference other workflows and connects their ports with edges.
Every workflow or plugin project should also include an agent skill under
.agent/skills/<skill-name>/SKILL.md. Update the skill whenever inputs,
outputs, runtime behavior, model requirements, or common commands change. Keep
the skill concrete: include at least one lfw run example and one HTTP
POST /workflows/{workflow_id}/run example using the shared run body.
Use a workflow collection when the repository should contain one or more workflow crates:
lfw init --workflowThis creates a project layout like:
Cargo.toml
.lightflow/
workspace.rs
workflows/
<short-name>/
Cargo.toml
src/
lib.rs
.agent/
skills/
<skill-name>/
SKILL.md
Then create a workflow crate inside the collection:
lfw new image_prompt --name "Image Prompt"Add .category("image") to workflow!() only when list/filter metadata is
useful. It does not change the generated or discovered crate path; workflow
crates remain direct children of the collection.
Older collections may still use workflows/<category>/<crate>. Migrate them
explicitly before using current discovery commands:
lfw migrateThe command preflights all moves and workspace member globs before flattening the project, global, and legacy collections. Conflicting target crate names cause the entire migration to stop without moving files.
Use a runtime-aware template when the workflow should execute through a known runtime capability:
lfw new image_generate --runtime lightflow.image.generateRuntime-aware templates include Node Schema metadata, a starter runtime declaration, an agent skill with CLI/API examples, and a contract test scaffold where applicable.
Use a plugin project when a repository should be a single Cargo crate that can
expose one workflow from src/lib.rs:
lfw init --pluginUse the global workflow home when a workflow should be available to many local projects without adding it to each project repository:
lfw new my_global_flow --globalProject-local workflows are discovered automatically from the current working
directory's workflows/ tree.
To add an external workflow crate as a dependency, use lfw add:
lfw add lightflow-text-prompt --version 0.1.0
lfw add lightflow-text-prompt --path projects/lightflow-std/workflows/text_prompt --editable
lfw add lightflow-text-prompt --git https://github.com/lightjunction/lightflow-std --package lightflow-text-promptYou can also use Cargo without a LightFlow wrapper:
cargo add lightflow-text-prompt
cargo add --path ../lightflow-text-prompt
cargo add --git https://github.com/example/my-workflow my-workflowlfw init --workflow makes the root manifest a non-publishable Cargo host
package with .lightflow/workspace.rs as its library target. Both cargo add
and lfw add therefore write ordinary root [dependencies]; each installed
library crate can own one workflow ID derived from its Cargo package metadata.
LightFlow uses cargo metadata to locate direct path, registry, and Git
dependency packages. A dependency with a library target containing
pub fn define() -> WorkflowSpec and workflow!() is loaded as a workflow;
only workflow dependencies are followed recursively. Ordinary dependencies do
not cause LightFlow to scan their full transitive graphs.
Use --editable for local development. It records a Cargo path dependency and
keeps edits live.
Use an external checkout path such as ../lightflow-std/workflows/text_prompt
only when lightflow-std is not checked out under projects/.
To import a repository that contains many workflow crates, use lfw import:
lfw import --global projects/lightflow-flux
lfw import --global https://github.com/lightjunction/lightflow-flux.gitUse add when the dependency target is one known Cargo package. Use
import when the target is a workflow repository or collection and LightFlow
should discover all workflow crates under workflows/<crate>/.
In practice:
lfw add lightflow-text-template --path projects/lightflow-std/workflows/text_templateadds one workflow crate.lfw import projects/lightflow-stdscans the repository and adds every workflow crate it finds underworkflows/*.
Global installs are written into the default LightFlow home, usually
~/.lightflow, or another directory listed in LFW_PATH.
That home is a normal Cargo workspace, not a custom package database. Global
install commands edit the home's Cargo.toml; Cargo still owns dependency
resolution and Cargo.lock.
Refresh dependency resolution with Cargo-backed commands:
lfw update
lfw upgrade
lfw update --global
lfw upgrade --globalCheck what LightFlow can discover:
lfw list
lfw ls --detail
lfw info
lfw help <workflow_id>An executable workflow uses the same Cargo package and the same src/lib.rs
define() function as a reusable library workflow. Add src/bin/<name>.rs:
fn main() -> lightflow::runner::RunnerResult<()> {
lightflow::runner::run_workflow_from_env(my_workflow::define())
}Install the binary through Cargo:
cargo install my-workflow --bin my-workflowThere is no separate executable-workflow archive or manifest format.
Publish reusable and executable workflow packages with cargo publish.
lfw publish <workflow_id> provides LightFlow validation and Cargo dry-run
gates, but Cargo remains the publisher.
A minimal leaf workflow declares metadata and ports:
use lightflow::preload::*;
pub fn define() -> WorkflowSpec {
workflow!()
.name("Text Echo")
.description("Return the input text unchanged.")
.input("text", "text")
.input_description("text", "Text to echo.")
.input_required("text", true)
.input_widget("text", "textarea")
.output("text", "text")
.output_description("text", "Echoed text.")
.build()
}The Cargo package is the workflow identity source. workflow!() expands in the
workflow crate and reads CARGO_PKG_NAME plus CARGO_PKG_VERSION. LightFlow
strips a leading lightflow-, converts remaining - characters to _, and
adds the lightflow. prefix. For example, package lightflow-text-echo
defines workflow lightflow.text_echo; changing the Cargo package version
changes the workflow version without editing src/lib.rs.
Port metadata follows Node Schema v1. Prefer adding it for user-facing workflows:
input_description/output_descriptionfor help and editor labels.input_requiredandinput_default_jsonfor validation and defaults.input_rangefor numeric sliders or steppers.input_enum_jsonfor select controls.input_widgetfor editor rendering hints such astextarea,prompt,image,file_save,json,toggle, ormodel_select.input_artifact_kind/output_artifact_kindfor artifacts such asimageandmask.input_model_requirement/output_model_requirementto bind ports to a declared model requirement.
A runtime-backed workflow declares a capability from the Executor Registry:
use lightflow::preload::*;
pub fn define() -> WorkflowSpec {
workflow!()
.name("Image Preview")
.description("Generate a deterministic preview image.")
.input("prompt", "text")
.input_description("prompt", "Prompt text.")
.input_required("prompt", true)
.input_widget("prompt", "prompt")
.input("output_path", "path")
.input_description("output_path", "Optional PNG output path.")
.input_required("output_path", false)
.input_widget("output_path", "file_save")
.input_artifact_kind("output_path", "image")
.output("image", "artifact")
.output_description("image", "Generated image artifact metadata.")
.output_artifact_kind("image", "image")
.output("image_path", "path")
.output_description("image_path", "Path to the generated image.")
.output_artifact_kind("image_path", "image")
.builtin_runtime("image_runtime", "lightflow.image.generate", "builtin.preview.v1")
.hf_model(
"image_model",
"preview-model",
"text-to-image",
"gguf",
"owner/repo",
"model.gguf",
)
.build()
}Use .runtime(id, capability) when any available executor for that capability
may run the workflow. Use .builtin_runtime(id, capability, engine) when the
workflow requires a specific builtin engine, such as builtin.preview.v1 or
builtin.llm.mock.v1.
Generate the API executor scaffold with:
lfw new comfy_run --runtime lightflow.comfyui.workflowExport a graph from ComfyUI with Save (API Format) and keep the prompt graph
inline in the run JSON. UI workflow JSON and a mutable workflow_path are not
accepted because replay must retain the exact submitted graph.
The following JSON is shape documentation only. Replace workflow with the
complete Save (API Format) export before running, and use only node ids that
exist in that complete graph:
{
"workflow": {
"<complete-api-format-graph>": "REPLACE_ME"
},
"node_inputs": {
"<node-id-from-your-complete-graph>": {"seed": 42}
}
}For image-to-image or inpainting, upload and bind source files after node input overrides:
{
"uploads": [
{"path": "input.png", "bind": [{"node_id": "<load-image-node-id>", "input": "image"}]},
{"path": "mask.png", "type": "temp", "bind": [{"node_id": "<mask-node-id>", "input": "image"}]}
]
}Merge that upload fragment into the complete run object; the binding ids must refer to actual image or mask inputs in the exported graph.
Run it with lfw run lightflow.comfy_run --inputs @comfy-run.json. Set
server_url, LIGHTFLOW_COMFYUI_URL, or rely on
http://127.0.0.1:8188. Optional Authorization comes only from
LIGHTFLOW_COMFYUI_AUTHORIZATION; it is not recorded. ComfyUI manages its own
models and custom nodes, so do not add fake LightFlow model requirements merely
to make the node card look model-backed.
Uploads and output_dir must stay beneath the project root after
canonicalization, so traversal and symlink escapes fail before network access.
Authorization is sent only when the resolved endpoint has the same origin as
configured LIGHTFLOW_COMFYUI_URL. A single deadline covers hashing, streaming
multipart upload, submit, polling, and streaming download; downloads refuse to
overwrite an existing artifact.
Run conformance before publishing or installing:
lfw node test lightflow.image_previewThis checks workflow validation, lfw help, Node Schema metadata, model
bindings, runtime executor availability, and the workflow crate's agent skill.
Generated placeholder descriptions are reported as node.placeholders
warnings here, so agents can spot incomplete metadata before the stricter
publish gate fails.
Before publishing, replace generated TODO workflow, input, and output
descriptions; lfw publish reports those placeholders as publish blockers for
workflow crates. It also checks normal, build, dev, and target-specific Cargo
dependency sections for crates.io blockers such as git dependencies and path
dependencies without versions, including dependencies inherited from
[workspace.dependencies] through workspace = true.
Before handing off agent-authored changes, run lfw loop changes to confirm
workflow crate edits and colocated skill edits are paired in the same review.
Tools can read the same report from /loop/changes or
lightflow.loop.changes.
Use lfw dev check for the broader developer gate plan before handoff. It
reuses the release gate definitions, but presents them as a daily development
workflow: local loop readiness, source-change safety, sibling project
workspaces, publish readiness, formatting, clippy, tests, workflow skill
coverage, and feature-specific runtime checks.
Use lfw dev check --project <name> when the current change is scoped to one
linked project workspace. The report still plans the normal developer gates,
but the project workspace review and lfw loop projects commands are
filtered to that workspace. <name> may be a workspace name, short alias,
label such as projects/lightflow-std, relative path such as
./projects/lightflow-std, or absolute checkout path. A project name that
matches no linked workspace fails the gate and reports the known workspace
names and aliases.
When a workflow skill is missing required usage guidance, run
lfw dev skill-template <workflow_id> to generate a compliant starter
SKILL.md with frontmatter, workflow contract notes, a CLI run example, and
an HTTP run example. Add --write to create it under the workflow crate's
.agent/skills/<skill-name>/SKILL.md; existing files are not overwritten
unless --force is also passed.
Use lfw dev project-config-template to inspect a starter
projects/lightflow-projects.toml, and add --write to create it when a
project set should stop relying on built-in compatibility defaults. Existing
config files are not overwritten unless --force is passed. The command still
returns a repair template when the existing config is invalid, so
--write --force can replace a broken project-set config intentionally. The
same response includes project_config_template_command,
project_config_write_command, and project_submodule_update_command for
repair prompts and configured submodule initialization.
lfw dev check and lfw release check also expose project_config_valid,
project_config_error, and the same template/write commands, so editor and
agent clients can surface a repair action from the first gate report.
The development profile skips release-only artifact and changelog-section
checks; those remain part of lfw release check.
lfw publish <workflow_id> --apply and lfw publish --workflows --apply run
the same gate before invoking Cargo publish commands.
Composite workflows use .node() to instantiate another workflow and .edge()
to connect output ports to input ports.
use lightflow::preload::*;
pub fn define() -> WorkflowSpec {
workflow!()
.name("Prompted Image")
.description("Render a prompt template, then generate an image.")
.input("topic", "text")
.input_description("topic", "Subject to render.")
.input_required("topic", true)
.input_widget("topic", "text")
.output("image", "artifact")
.output_description("image", "Generated image artifact.")
.output_artifact_kind("image", "image")
.output("image_path", "path")
.output_description("image_path", "Generated PNG path.")
.output_artifact_kind("image_path", "image")
.depends_on("lightflow.text_template", "0.1.0")
.depends_on("lightflow.text_to_image", "0.1.0")
.node("template", "lightflow.text_template")
.node("generate", "lightflow.text_to_image")
.edge("template", "text", "generate", "prompt")
.build()
}Workflow inputs are automatically visible to child nodes when the child has an
input with the same name. Use .edge(from_node, from_port, to_node, to_port)
when a child output should feed another child input.
Declare dependencies with exact versions so lfw deps can verify the graph:
lfw deps lightflow.prompted_imageUse install hints when a dependency may not already be installed:
workflow!()
.depends_on_path(
"lightflow.text_to_image",
"0.1.0",
"lightflow-text-to-image",
"projects/lightflow-std/workflows/text_to_image",
)
.depends_on_git(
"lightflow.text_template",
"0.1.0",
"lightflow-text-template",
"https://github.com/lightjunction/lightflow-std",
"lightflow-text-template",
)Then let LightFlow add missing Cargo dependencies:
lfw sync lightflow.prompted_image --applyRun one workflow:
lfw run lightflow.text_echo -i text='"hello"'Use JSON values for non-string inputs:
lfw run lightflow.control_merge \
-i a='{"prompt":"cat"}' \
-i b='{"seed":1}' \
-i mode='"object"'Pipe one workflow into another from the CLI:
lfw run lightflow.text_to_image \
-i prompt='"a quiet lake"' \
-i output_path='"out/lake.png"' \
'|' lightflow.image_invert \
-i output_path='"out/lake-inverted.png"'Use lfx as a short alias for lfw run:
lfx lightflow.text_to_image --text "a quiet lake" --output out/lake.pngBefore committing a workflow change:
- Keep
src/lib.rsas the source of truth for workflow metadata. - Add Node Schema metadata for user-facing ports.
- Declare runtime capabilities and model requirements explicitly.
- Add or update
.agent/skills/<skill-name>/SKILL.md. - Run
lfw help <workflow_id>. - Run
lfw node test <workflow_id>. - Run
lfw deps <workflow_id>for composite workflows. - Run representative
lfw run ...commands for executable workflows.