Getting Started
Build a workflow from scratch in five steps. By the end you will have registered a task, written a spec, compiled a workflow, and run it — both from the CLI and via an auto-generated web form.
Prerequisites
- Python 3.10 or later
- uv — for package development and running the compiler
- pixi — required to run compiled workflows
wt-compiler— install withuv pip install wt-compilerorpixi global install wt-compiler
Step 1 — Create a task package
The compiler discovers tasks by installing packages into an ephemeral
environment and running wt-registry as a subprocess. Your tasks must live in
an installable Python package.
Directory structure
custom-tasks/
├── pyproject.toml
└── src/
└── custom_tasks/
├── __init__.py
└── tasks.py
pyproject.toml
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
[project]
name = "custom-tasks"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"wt-registry",
]
[tool.setuptools.packages.find]
where = ["src"]
[project.entry-points."wt_registry"]
custom-tasks = "custom_tasks.tasks"
[tool.uv.sources]
wt-registry = { path = "../../../wt-registry", editable = true }
The [project.entry-points."wt_registry"] section tells wt-registry which
module to import when discovering tasks. By convention, use your package name as
the entry-point key (custom-tasks). The value (custom_tasks.tasks) is the
Python module containing your @register-decorated functions. Without this
entry point, the compiler will not find your functions.
src/custom_tasks/tasks.py
Every registered function must have complete type annotations on all parameters and on the return type. These annotations drive JSON schema generation for web forms and compile-time validation.
"""Simple tasks used in the Getting Started guide and tutorials."""
from wt_registry import register
@register(description="Add two integers.")
def add(a: int, b: int) -> int:
return a + b
@register(description="Double a number.")
def double(n: int | float) -> int | float:
return n * 2
@register(description="Split an integer into its individual digits as strings.")
def split_digits(n: int) -> list[str]:
return list(str(abs(n)))
@register(description="Parse a string as an integer.")
def parse_int(s: str) -> int:
return int(s)
The @register decorator auto-generates a title from the function name
(add becomes Add), stores the entry in a global registry, and returns the
original function unchanged.
Install and verify
uv venv
uv pip install -e ./custom-tasks
uv run wt-registry --package custom_tasks --format pretty
You should see each function listed with its title, description, and import path.
Expected --format pretty output
=== custom_tasks.tasks.add ===
Title: Add
Description: Add two integers.
Deprecated: No
Import: from custom_tasks.tasks import add
=== custom_tasks.tasks.double ===
Title: Double
Description: Double a number.
Deprecated: No
Import: from custom_tasks.tasks import double
=== custom_tasks.tasks.split_digits ===
Title: Split Digits
Description: Split an integer into its individual digits as strings.
Deprecated: No
Import: from custom_tasks.tasks import split_digits
=== custom_tasks.tasks.parse_int ===
Title: Parse Int
Description: Parse a string as an integer.
Deprecated: No
Import: from custom_tasks.tasks import parse_int
Expected --format json output (what the compiler consumes)
Use --format json (the default) to see the machine-readable output.
The compiler consumes this JSON to generate web forms and perform
input validation — each entry's json_schema defines the parameter
types and constraints that drive the auto-generated UI.
uv run wt-registry --package custom_tasks --format json
{"entries":{"custom_tasks.tasks.add":{"metadata":{"title":"Add","description":"Add two integers.","tags":[],"deprecated":false,"deprecation_message":null},"module_path":"custom_tasks.tasks","public_module_path":"custom_tasks.tasks","function_name":"add","import_statement":"from custom_tasks.tasks import add as add","json_schema":{"additionalProperties":false,"properties":{"a":{"title":"A","type":"integer"},"b":{"title":"B","type":"integer"}},"required":["a","b"],"type":"object"}},"custom_tasks.tasks.double":{"metadata":{"title":"Double","description":"Double a number.","tags":[],"deprecated":false,"deprecation_message":null},"module_path":"custom_tasks.tasks","public_module_path":"custom_tasks.tasks","function_name":"double","import_statement":"from custom_tasks.tasks import double as double","json_schema":{"additionalProperties":false,"properties":{"n":{"anyOf":[{"type":"integer"},{"type":"number"}],"title":"N"}},"required":["n"],"type":"object"}},"custom_tasks.tasks.split_digits":{"metadata":{"title":"Split Digits","description":"Split an integer into its individual digits as strings.","tags":[],"deprecated":false,"deprecation_message":null},"module_path":"custom_tasks.tasks","public_module_path":"custom_tasks.tasks","function_name":"split_digits","import_statement":"from custom_tasks.tasks import split_digits as split_digits","json_schema":{"additionalProperties":false,"properties":{"n":{"title":"N","type":"integer"}},"required":["n"],"type":"object"}},"custom_tasks.tasks.parse_int":{"metadata":{"title":"Parse Int","description":"Parse a string as an integer.","tags":[],"deprecated":false,"deprecation_message":null},"module_path":"custom_tasks.tasks","public_module_path":"custom_tasks.tasks","function_name":"parse_int","import_statement":"from custom_tasks.tasks import parse_int as parse_int","json_schema":{"additionalProperties":false,"properties":{"s":{"title":"S","type":"string"}},"required":["s"],"type":"object"}}},"version":"1.0.0"}
For more on the decorator, validation rules, and JSON schema generation, see the wt-registry reference.
Step 2 — Your first spec.yaml
A spec declares which tasks to run and how data flows between them. Here is the simplest possible workflow: one task, all parameters provided by the user at runtime.
id: add_two_numbers
requirements:
- name: custom-tasks
path: /absolute/path/to/custom-tasks
workflow:
- id: total
name: "Add Two Numbers"
task: custom_tasks.tasks.add
Replace the path
Change /absolute/path/to/custom-tasks to the actual absolute path to
your custom-tasks/ directory.
Local paths are for development only
The path: requirement installs a package from your local filesystem.
Compiled workflows that use path: will not work on other machines.
For distributable workflows, use git: or conda channel requirements
instead — see Distributing workflows.
Let's walk through the spec field by field:
id— a unique identifier for the workflow. The compiler uses this to name the output directory (wt-<id>-workflow).requirements— the packages the compiler must install to discover tasks. Each entry has anameand a source (path:,git:, or a conda channel).workflow— an ordered list of task instances. Each entry has:id— identifies this task instance within the workflow. Used in${{ }}references and as the key in--config-json.name— a human-readable label (used in web forms and logs).task— the fully-qualified import path of the registered function.
For the full spec syntax, see the spec.yaml reference.
Step 3 — Compiling
Run the compiler to turn your spec into an executable workflow:
wt-compiler compile --spec spec.yaml
The compiler:
- Resolves
requirementsinto a temporary environment. - Discovers registered functions by running
wt-registryas a subprocess. - Validates the spec against the discovered function schemas.
- Generates a self-contained pixi workspace with DAG code, parameter
schemas, a
pixi.toml, and a Dockerfile.
The output directory is named wt-add-two-numbers-workflow. Take a look inside:
wt-add-two-numbers-workflow/
├── pixi.toml
├── Dockerfile
├── ...
├── wt_add_two_numbers_workflow/
│ ├── dags/ # Generated DAG code
│ ├── params.json # Flat parameter schema (CLI)
│ ├── rjsf.json # Hierarchical schema (web forms)
│ └── ...
└── tests/
└── ...
Two schema files are generated from your type annotations:
rjsf.json— hierarchical schema withuiSchemafor form layout. RJSF-compatible.params.json— flat schema for CLI usage (--config-json/--config-file).
Both describe the same parameters; only the structure differs. For full detail on compiled artifacts, see the wt-compiler reference.
Step 4 — Configuration and running
Compiled workflows write their output to a results directory controlled by an environment variable:
mkdir results
export WT_RESULTS=file://$(pwd)/results
CLI
cd wt-add-two-numbers-workflow
pixi run wt-add-two-numbers-workflow run --config-json '{"total": {"a": 1, "b": 2}}'
Two run commands?
pixi run launches a command inside the pixi environment. The second run
is the workflow CLI's own run subcommand. Together:
pixi run <entrypoint> run --config-json ....
Expected result (contents of result.json in your results directory):
{"result": 3, "error": null, "trace": null}
- Both parameters (
aandb) are unbound, so they become user-facing configuration. - The
--config-jsonkey"total"matches the task instanceidin the spec.
Web form
The compiler also generated an rjsf.json schema from your type annotations.
Here is what the auto-generated form looks like — try entering values for a
and b and watch the formData update live:
rjsf.json
{
"properties": {
"total": {
"type": "object",
"title": "Add Two Numbers",
"properties": {
"a": { "type": "integer", "title": "A" },
"b": { "type": "integer", "title": "B" }
},
"required": ["a", "b"],
"additionalProperties": false
}
},
"uiSchema": {
"total": {
"ui:order": ["a", "b"]
},
"ui:order": ["total"]
},
"additionalProperties": false
}
The pipeline: Type annotations → JSON Schema → RJSF web form.
The form is generated entirely from the type annotations on your add
function. For the full story on how the compiler produces form schemas and how
they relate to the CLI, see
Concepts — Configuration and execution.
Step 5 — What's next
You now know how to register tasks, write a spec, compile a workflow, run it from the CLI, and preview auto-generated web forms.
Continue learning:
- How-To Guides — partial arguments, chaining task outputs,
mapfan-out, and more - spec.yaml reference — complete field-by-field
documentation including
map,mapvalues,skipif, and task groups - Concepts — the full mental model behind the framework
- wt-compiler reference — CLI options, compiled artifacts, and troubleshooting