Readiness: Implemented
The toolchain
Tasks
Define project commands in uf.config.js. uf run runs a task and its
dependencies, with parallel execution and optional caching.
Start with a uf project. Project automation belongs in the
config's tasks field; uf install rejects package.json scripts.
Define tasks
A task can be a command string or an object with options. This example assumes the project has scripts for generating tokens, icons and an asset summary:
// @flow
import { defineConfig } from "@uniflowed/config";
export default defineConfig({
tasks: {
tokens: {
command: "node scripts/tokens.js",
inputs: ["tokens/**", "scripts/tokens.js"],
outputs: ["generated/tokens.css"],
},
icons: {
command: "node scripts/icons.js",
inputs: ["icons/**", "scripts/icons.js"],
outputs: ["generated/icons.txt"],
},
assets: { command: "node scripts/done.js", dependsOn: ["tokens", "icons"] },
greet: "node scripts/greet.js",
},
});
| Field | Purpose |
|---|---|
command | Command to execute; omitting it delegates to Vite Task |
dependsOn | Tasks that must succeed first; use member#task for another workspace member |
inputs | File globs used to calculate the cache key; no inputs means no caching |
outputs | File globs checked before replaying a cached result |
env | Environment variables for the task |
cwd | Working directory relative to the project root |
cache | Set to false to disable caching |
args | Arguments the task takes, with names, choices and defaults |
Use task names that describe your workflow, such as verify or bundle.
Built-in commands such as uf fmt and uf lint are available directly.
Run uf run without a name to choose a task from a list. Type to filter, use
the arrow keys to move, and press Enter to run it. Outside a terminal, or with
--list, uf run prints the tasks instead:
uf run · site
─────────────
task runs after
assets node scripts/done.js tokens, icons
greet node scripts/greet.js
icons node scripts/icons.js
tokens node scripts/tokens.js
› 4 tasks; run one with `uf run <task>`
ufr is a shortcut for uf run. ufx is a shortcut for uf exec.
Both are installed with uf.
Run a task and its dependencies
$ uf run assets
icons | indexed 2 icons
tokens | wrote 2 tokens to generated/tokens.css
✓ tokens ran 0.02s
✓ icons ran 0.02s
done
✓ assets ran 0.02s
3 tasks, 0 replayed, 3 run, in 0.05s
tokens and icons run in parallel. assets starts after both succeed.
A shared dependency runs only once.
By default, uf runs up to four tasks at once, limited by the number of CPU cores.
Use -j N to change the limit; it cannot exceed the core count.
Dependency output is prefixed with the task name. The requested task's output
is printed directly. Status lines and the summary go to stderr. A single task
has no summary unless you pass --why.
Commands and arguments
uf starts a program and its arguments directly. Commands that use shell syntax,
such as pipes, &&, redirects, $VARIABLE, globs or cd, run through sh -c.
Those commands need a POSIX shell, including on Windows.
A leading NAME=value sets a variable without requiring a shell.
Tasks receive the mode's environment files, followed by the task's
env and any leading NAME=value assignments. Existing process variables take
priority over environment files. The default mode is development; change it
with --mode.
Arguments after the task name are appended to the command and parsed again. Quoted arguments may split, and shell characters can cause shell execution:
$ uf run greet -- --loud "two words"
arguments: ["--loud","two","words"]
Use -- to separate task arguments from uf options such as --why, --force,
--mode and -j. Other options after the task name reach the task without it.
Declare arguments
A task can declare the arguments it takes. Each one has a name, and optionally
a description, a list of choices, a default and required:
tasks: {
deploy: {
command: "node scripts/deploy.js",
args: [
{
name: "target",
description: "Where to deploy",
choices: ["staging", "production"],
},
{ name: "tag", description: "The release tag" },
{ name: "region", choices: ["us", "eu"], default: "eu" },
],
},
}
Pass values after the task name in the declared order, or by name as
--name value or --name=value. The two forms can be mixed. uf appends the
values to command in the declared order either way:
$ uf run deploy production v1.4.0
$ uf run deploy --tag v1.4.0 --target production
Both commands run node scripts/deploy.js production v1.4.0 eu. Each value is
one argument to the command, even if it contains spaces. Words that fill no
declared argument, such as --dry-run, follow the declared values unchanged.
A value outside choices is refused before anything runs:
$ uf run deploy prod v1.4.0
error: task "deploy": <target> cannot be "prod"; it is one of: staging, production
An argument is required unless it has a default or sets required: false.
If a required argument is missing and uf is running in a terminal, it asks for
it. An argument with choices opens a list you can filter by typing. Any other
argument opens a text prompt. uf then prints the full command and the
equivalent uf run command line, then runs the task:
$ uf run deploy
deploy · target — Where to deploy
› type to filter
❯ staging
production
↑↓ move · ⏎ run · esc cancel
› deploy: node scripts/deploy.js production v1.4.0 eu
next time: uf run deploy --target production --tag v1.4.0 --region eu
uf does not ask for arguments that have a default. Leave out the default if you
want to choose the value every time. Choosing a task from uf run or uf with
no arguments also asks for its missing arguments.
uf never asks outside a terminal. This includes CI, pipes and redirected input. A missing argument is an error that names the argument and its choices:
$ uf run deploy
error: task "deploy": missing <target>, <tag>
<target> Where to deploy — one of: staging, production
<tag> The release tag
pass them after the task's name, in this order — `uf run deploy <target> <tag> [region=eu]` — or by name, as in `uf run deploy --target <value>`. At a terminal, uf asks for them.
uf run --list shows each task's arguments after its command. Shell
completion offers the choices of the next argument and the names of arguments
that have not been given.
Some combinations are refused before a task runs. These include a name that
cannot be written as --name, a repeated name, and a default outside
choices. An optional argument without a default must come after every
argument that always has a value. Otherwise, leaving it out would shift later
values into its position. An argument named after a uf run option, such as
mode, can only be passed by name after --.
The values are part of the command, so they are part of the
cache key. uf prepare does not fill declared arguments. It
appends staged files after the arguments' defaults, and fails if an argument
has no default.
Cache results
An object task is cacheable when it has non-empty inputs and cache is not
false. The cache key includes:
- The task name, command and arguments.
- The mode, resolved environment-file values, task
envandcwd. - The
inputspatterns and the contents of matching files. - The
outputspatterns.
A matching result replays the previous output without running the command.
Use --why to see each decision:
$ uf run assets --why
icons | indexed 2 icons
tokens | wrote 2 tokens to generated/tokens.css
✓ icons replayed saved 0.02s — every declared input is unchanged
✓ tokens replayed saved 0.02s — every declared input is unchanged
done
✓ assets ran 0.02s — declares no inputs, so it always runs
3 tasks, 2 replayed, 1 run, in 0.02s
Changing an input or deleting an output runs the affected task again:
✓ tokens ran 0.02s — tokens/colors.json changed
✓ icons ran 0.02s — generated/icons.txt is missing
Outputs are checked, not restored. Every recorded output must still exist with
the same content. Failed runs are not cached. --force reruns tasks and records
fresh results. Cache records live in .uf/cache/task/; uf clean removes
.uf/, including this cache.
The --why metadata stores environment variable names and value digests,
not raw values. Digests of predictable values can still be guessed.
Cache limits
Account for these when choosing inputs:
- Process environment: exported shell variables are not part of the key.
Declare values that affect a task in its
env. - Toolchain and platform: the uf version, JavaScript host, OS and shell are not included. Disable caching when those can change the result.
- Dependencies in the same project: a dependency rerunning does not
invalidate its dependents. Include the files it generates in their
inputs. Cross-member dependencies also include the dependency result. - Excluded directories:
**skips.git,.uf,.uniflowed,dist,node_modulesandtarget, and does not follow symlinks. Name an excluded directory with its literal path:npm/ui/dist/**works;npm/*/dist/**does not.
Handle failures
$ uf run release
tokens | wrote 3 tokens to generated/tokens.css
✓ tokens replayed saved 0.02s
schema | schema: the field `id` is declared twice
✗ schema ran 0.02s
3 tasks, 1 replayed, 1 run, 1 not reached, in 0.02s
error: task "schema" exited with exit status: 3
After a failure, no new task starts. Tasks already running finish.
uf run exits with status 1 and reports the failed task's exit code.
Use uf exec when you need to return a child program's exit code directly.
Before execution, uf rejects dependency cycles, missing tasks and requests to cache a task without inputs:
error: `dependsOn` in uf.config.js closes a loop: loop-a → loop-b → loop-a
each of these waits for the next, so none of them can start
error: task "asets" is not defined in uf.config.js
did you mean: assets
Only tasks reachable from the requested task are checked for cycles.
Workspaces
A workspace member is a directory with uf.config.js, or a package listed in
the root's package.json workspaces. Each member defines its own tasks.
uf run#site build runs build in the site member.
uf run build -r runs it in every member that defines it:
$ uf run build -r
✓ utils#build ran 0.02s
✓ ui#build ran 0.02s
✓ app#build ran 0.02s
3 tasks, 0 replayed, 3 run, in 0.06s
Members run after their package dependencies. uf uses dependencies,
devDependencies, peerDependencies and optionalDependencies, matched by
package name. Independent members run in parallel under the same -j limit.
The root's own tasks are excluded from -r; run those without -r.
--filter selects members and implies -r. Repeat it to combine selections:
--filter | Selects |
|---|---|
ui | Member named ui or at path ui |
@acme/*, npm/* | Members matching the name or path glob |
./npm/ui | Member at that path; globs after ./ match paths only |
app... | app and its dependencies |
app^... | Only the dependencies of app |
...ui | ui and its dependents |
...^ui | Only the dependents of ui |
A filter that matches no member is an error.
Use dependsOn: ["ui#build"] to depend on another member's task. The member can
be named by package name or path. This works from the root or another member.
A task name containing # still takes precedence when that exact task exists
in the config.
Each member keeps its cache in its own .uf/cache/task/. Running a task directly
or through -r uses the same records. Cross-member dependencies add their
result to the dependent task's cache key. If a dependency has no cache, its
dependent always runs.
For a run across multiple members, --why prints the graph and explains
changes caused by dependencies:
$ uf run build -r --why
3 tasks across 3 packages
utils#build
ui#build after utils#build
app#build after ui#build
✓ utils#build replayed saved 0.02s — every declared input is unchanged
✓ ui#build ran 0.02s — src/index.js changed
✓ app#build ran 0.02s — ui#build changed
3 tasks, 1 replayed, 2 run, in 0.04s
uf explain run lists workspace members, task counts and execution order.
A task with no command
An object without command delegates to Vite Task. uf starts vp run <name>,
or the program set by UF_VITE_TASK_BIN, in the project root.
The command must be installed. uf's scheduling and cache do not apply to it.
vp run <name> runs the package.json script of that name, which is not a
command anybody wrote in uf.config.js. A project that wants every task it
runs spelled out there says taskRunner: { allowPackageScripts: false }, and a
task with no command then stops the run with an error naming it.
Run a package binary
uf exec, or ufx, runs from the project root with its environment values and
returns the program's exit code. It looks for:
- Built-in handlers for
@uniflowed/create,@uniflowed/testand@uniflowed/pm. - A binary in the project's
node_modules/.bin. - A file path.
It does not search parent projects or PATH, and does not download a missing
package by default:
$ ufx left-pad
error: left-pad is not installed in this project, and fetching it would run code uf.lock does not pin.
Declare it in package.json and run `uf install`, or say so explicitly:
`uf exec --yes left-pad`, which runs `npx --yes left-pad`.
Pass --yes to let the project's package manager fetch it using npx --yes,
pnpm dlx, yarn dlx or bunx. --yes is a uf option even after the package
name. A bare -- after the name is removed; other arguments reach the binary.
Current limits
uf run has no JSON output, dry run, task timeout or remote cache. Cached
outputs are not restored from storage; missing files trigger a new run.
A declared argument is always appended in order. It cannot be placed in the
middle of command, and it has no boolean or repeated form.
Next steps
Agents is next: the same commands, run by a coding agent. uf in CI shows how to run checks in a pipeline, and Environments explains environment files and modes. See the task configuration and CLI reference for all options.
Edit this pagedocs/app/guide/tasks/$page.mdx